ãã¡ãããç§ã¯WPF UIã®éçºè ãšããŠããã¹ãŠã®PDFããªããã£ãã®ã¬ã³ããªã³ã°ãCïŒã³ãŒãã§ã³ãŒãã£ã³ã°ãããšããå³ããã¢ãããŒãã«ããã«å察ããŸããã
ãŸãã顧客ã¯ãããšãã°HTMLããPDFãžã®ç¹å®ã®ææã³ã³ããŒã¿ãŒã®è³Œå ¥ã«å察ããŸããã§ããã
ãã¹ãŠãã·ã³ãã«ã«æããŸã-ãã³ãã¬ãŒãçæã«DotLiquidã䜿çšããŠHTMLããŒã¯ã¢ãããå«ãè¡ãçæããå€ãã®ææã³ã³ããŒã¿ãŒã®ããããã䜿çšããŠPDFã«å€æããŸãã
å¯äžã®åŸ ã¡äŒãã¯ãPDFããã¥ã¡ã³ãã®ããŒãžæ§é ãšã®HTMLäºææ§ã®äœãã§ãã
ãã®åé¡ã®è§£æ±ºçãæ¢ãå§ããŠããã«ãããååãå¥ã®è§£æ±ºçãããèšäºãžã®ãªã³ã¯ãå ±æããŸããã
ãã®èšäºãããXPSããã¥ã¡ã³ãããPDFãçæã§ããããšãåŠã³ãŸããïŒãã®åœ¢åŒã¯WPF FlowDocumentã§ãµããŒããããŠããŸãïŒã
ããã«ãç¡æã®PDFSharpã©ã€ãã©ãªãŒãçæã«äœ¿çšãããŸããã
ãœãŒã¹ã¯GitHubããããŠã³ããŒãã§ããŸã ã
UPD ïŒ ã«ã«ããããŒãžããªãããèšäºãã©ã®ããã«è¿œå ããããã芳å¯ããã®ã¯ãããåããŠã§ã¯ãããŸããïŒæåã®æ¬ ç¹ã¯çºè¡çŽåŸã§ãããã¡ã€ã³ã³ã³ãã³ãã«é¢é£ããå¯èœæ§ã¯äœãïŒã åæ©ä»ãããã£ãŒãããã¯ã«èå³ããããŸãã å°é£ã§ã¯ãªãã«ããŠãã誰ãäœã«äžæºãæã£ãŠããããéäŒããŠãã ããã
å 責äºé
泚æãåèµ·ãããœãŒã¹ã³ãŒãã¯ãåŸãã¹ãäŸã§ã¯ãããŸããã èšäºãåŒããããªãããã«ããããããã¶ã€ã³ãã¿ãŒã³ã«ã¯åŸããŸããã§ããã ãœãŒã¹ã³ãŒãã¯ãåçŽãªãã³ãŒãããã€ã³ããã¢ãããŒãã§ãã ããã¯ãæ¬è³ªã®èªèã容æã«ããããã«ãè¡ãããŸãã PDFçæèªäœã«çŠç¹ãåãããŸãã äž»ãªã³ãŒãããããžã§ã¯ãã®æ§é ã«ç°¡åã«çµ±åã§ãããšæããŸãã
ãŸãããœãŒã¹ã§ã¯ãDotLiquidãã³ãã¬ãŒãã®ããŒã¿ãœãŒã¹ãšããŠã®dynamicã®å€§èŠæš¡ãªäœ¿çšã«ééããŸãã ãããäž»ã«åçŽããšé床ã®ããã«è¡ãããŸããã DotLiquid Webãµã€ãã«ã¯ããã³ãã¬ãŒãã§äœ¿çšã§ããããã«ç¬èªã®ã¯ã©ã¹ã«æ³šéãä»ããæ¹æ³ã®èª¬æããããŸãã ããã§ãç§ã®ãœãŒã¹ãããŒãºã«ç°¡åã«é©åãããããšãã§ããŸãã
ãŸããPDFSharpãFlowDocument / XPSæ¬äŒŒãã©ã³ãã®åé¡ãæ€åºããããšã«ãèšåãã䟡å€ããããŸãã ç¹ã«ãã¬ã³ããªã³ã°ãããXPSã®ã¬ã³ããªã³ã°ãããŠããªããªã¹ãããŒã«ãŒã¯ã空ã®åè§åœ¢ãšããŠPDFã«ãšã¯ã¹ããŒããããŸãã ãããã°ã¢ãŒãã§ããã©ã³ãã®ã€ã³ããŒã/ãšã¯ã¹ããŒããšã©ãŒãçºçããDebug.AssertïŒ...ïŒã¡ãã»ãŒãžãåãåããŸããã ãã®åé¡ã¯ãŸã 調æ»ãããŠããŸããã ãªã¹ãã®åé¡ã¯ãã³ãã¬ãŒãã§ç°¡åã«åé¿ã§ããŸãã
æºåãã
以äžã¯ãå¿ èŠãªæäœã®ãªã¹ãã§ãã
- å€æŽãããPDFSharpã«é¢ãããµã€ãã«ã¢ã¯ã»ã¹ããããããã³ã³ãã€ã«ãããã¢ã»ã³ããªãŸãã¯ãœãŒã¹ã³ãŒãèªäœãããŠã³ããŒãããŸãã å¥ã®æ¹æ³ã¯ãPDFSharpããŒãžã§ã³1.2ã1.31ã§ãã
- NuGetã䜿çšããŠDotLiquidã©ã€ãã©ãªïŒå·çæç¹ã§ã¯ããŒãžã§ã³1.7.0ïŒãã€ã³ã¹ããŒã«ããŸãïŒãŸã ã€ã³ã¹ããŒã«ããŠããªãå Žåã¯Nugetãã€ã³ã¹ããŒã«ããŸãïŒ
- System.Printingããã³ReachFrameworkã¢ã»ã³ããªãžã®åç §ããPDFãçæããããããžã§ã¯ãã«è¿œå ããŸãã
ã¡ã€ã³ãŠã£ã³ããŠ
以äžã¯ã¡ã€ã³ãŠã£ã³ããŠã®ã¬ã€ã¢ãŠãã§ãã
<Window x:Class="Solution.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="480" Width="640"> <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <FlowDocumentReader x:Name="DocViewer"> <FlowDocument> <FlowDocument.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14"/> <Setter Property="Margin" Value="5"/> </Style> </FlowDocument.Resources> <BlockUIContainer> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Ellipse Fill="#003481" Width="5" Height="5" Margin="5"/> <TextBlock Text="Title" FontWeight="Bold" Grid.Column="1"/> <TextBlock Text="Description" Grid.Column="2"/> </Grid> </BlockUIContainer> </FlowDocument> </FlowDocumentReader> <StackPanel Grid.Row="1" Orientation="Horizontal"> <Button Click="ParseButton_OnClick">Parse</Button> <Button Click="ButtonBase_OnClick">Print</Button> </StackPanel> </Grid> </Window>
ããã«ã¯ãã¬ã³ããªã³ã°ãããFlowDocumentã衚瀺ããFlowDocumentReaderããããŸãã ããŒã¯ã¢ããã§ã¯ãVisual Studioã®ãã¶ã€ããŒã䜿çšããŠãã³ãã¬ãŒããäœæããããã«äœ¿çšããããŒãã³ãŒããããFlowDocumentã確èªã§ããŸãã
ãŸããéåžžã®ã³ã³ãããŒã«ãšWPFã¹ã¿ã€ã«ã䜿çšããŠããããšãããããŸãã ããã¯ãFlowDocumentã䜿çšããŠPDFãçæããããšã®å€§ããªããŒãã¹ã®1ã€ã§ãã WPFã¢ããªã±ãŒã·ã§ã³ã®ã³ã³ãããŒã«ãšã¹ã¿ã€ã«ãªãœãŒã¹ã䜿çšã§ããŸãã HTMLã仲ä»ãšããŠäœ¿çšããã¢ãããŒãã®å ŽåãCSSã¹ã¿ã€ã«ãšHTMLã®äžéšã®ã¢ã»ã³ããªãåå¥ã«ãµããŒãããå¿ èŠããããŸããããããã¯ãŸã äœããã®åœ¢ã§ãã³ãã¬ãŒãã«åã蟌ãå¿ èŠããããŸãã
ãã³ãã¬ãŒãã®ããŒã¿ã³ã³ããã¹ã
ããŒã¿ã³ã³ããã¹ããçæããããã«ãã¡ã€ã³ãŠã£ã³ããŠã®ã³ãŒãããã€ã³ãã«ãã©ã€ããŒãã¡ãœãããè¿œå ããŸããããã®ã¡ãœããã§ã¯ãåçãªããžã§ã¯ãã®DotLiquid.Hashã®äœæãããŒãã³ãŒãã£ã³ã°ãããŠããŸãã
private DotLiquid.Hash CreateDocumentContext() { var context = new { Title = "Hello, Habrahabr!", Subtitle = "Experimenting with dotLiquid, FlowDocument and PDFSharp", Steps = new List<dynamic>{ new { Title = "Document Context", Description = "Create data source for dotLiquid Template"}, new { Title = "Rendering", Description = "Load template string and render it into FlowDocument markup with Document Context given"}, new { Title = "Parse markup", Description = "Use XAML Parser to prepare FlowDocument instance"}, new { Title = "Save to XPS", Description = "Save prepared FlowDocument into XPS format"}, new { Title = "Convert XPS to PDF", Description = "Convert XPS to WPF using PDFSharp"}, } }; return DotLiquid.Hash.FromAnonymousObject(context); }
å 責äºé ã«æžããããã«ãããã¯ã»ãã®äžäŸã§ãã å®éã®ãããžã§ã¯ãã§ã¯ãå®éã®DTOãŸãã¯ViewModelçšã®äœããã®ã³ã³ããŒã¿ãŒãå¿ èŠã§ãã
DotLiquidããŒãžã®éçºè ããã¥ã¢ã«ã«ã¯ ããã³ãã¬ãŒãã®æååå€ã衚瀺ããããã«ãä»»æã®ã¯ã©ã¹ã®ã€ã³ã¹ã¿ã³ã¹ã䜿çšããããšã¯ã§ããªããšæžãããŠããŸãã ããšãã°ããã³ãã¬ãŒãã®DateTimeãªããžã§ã¯ãã®åºåãèšè¿°ããå Žåããã©ã¡ãŒã¿ãŒãªãã®ToStringïŒïŒã®åºåã¯ãã¬ã³ããªã³ã°ãããããã¥ã¡ã³ãã«å ¥ããŸãã ãã ããäœæãããªããžã§ã¯ããããšãã°BlaBlaUserãæå¹ã«ãªã£ãŠããå ŽåãDotLiquidã¯ä»£ããã«ãšã©ãŒæååã衚瀺ããŸãã ãããŠãããã¯ããšããã§ãéåžžã«è¯ãã§ãããªããªã ãã³ãã¬ãŒãã¯åŒãç¶ãã¬ã³ããªã³ã°ãããŸãããééããç¹å®ã®å Žæãããã«è¡šç€ºãããŸãã
æš¡æ§
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <FlowDocument.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14"/> <Setter Property="Margin" Value="5"/> <Setter Property="TextWrapping" Value="Wrap"/> </Style> </FlowDocument.Resources> <Paragraph FontSize="24"> <Bold>{{ Title }}</Bold> </Paragraph> <Paragraph FontSize="16"> {{ Subtitle }} </Paragraph> <Paragraph FontSize="16"> <Bold>Steps to generate PDF:</Bold> </Paragraph> {% for step in Steps -%} <BlockUIContainer> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Ellipse Fill="#003481" Width="5" Height="5" Margin="5"/> <TextBlock Text="{{ step.Title }}" Foreground="#003481" FontWeight="Bold" Grid.Column="1"/> <TextBlock Text="{{ step.Description }}" Grid.Column="2"/> </Grid> </BlockUIContainer> {% endfor -%} </FlowDocument>
TextBlock.Textå±æ§ã§DotLiquidã³ã³ããã¹ãã«çŽæ¥ãã€ã³ãã£ã³ã°ãæ¿å ¥ãã代ããã«ããã¹ããããCDATAãããã¯ã䜿çšããæ¹ãå®å šã§ããããšã«æ³šæããŠãã ããã
<TextBlock Foreground="#003481" FontWeight="Bold" Grid.Column="1"> <![CDATA[ {{ step.Title }} ]]> </TextBlock>
ããã«ãããXML圢åŒãšäºææ§ã®ãªãæåããä¿è·ãããŸãã
FlowDocumentã®ã¬ã³ããªã³ã°ãšè§£æ
private void ParseButton_OnClick(object sender, RoutedEventArgs e) { using (var stream = new FileStream("Templates\\report1.lqd", FileMode.Open)) { using (var reader = new StreamReader(stream)) { var templateString = reader.ReadToEnd(); var template = dotTemplate.Parse(templateString); var docContext = CreateDocumentContext(); var docString = template.Render(docContext); DocViewer.Document = (FlowDocument) XamlReader.Parse(docString); } } }
ããã§ã¯ãã¹ãŠãç°¡åã§ãã ãã³ãã¬ãŒãã§ãã¡ã€ã«ã¹ããªãŒã ãéãããã³ãã¬ãŒãã³ã³ããã¹ããäœæããŠFlowDocumentããŒã¯ã¢ãããã¬ã³ããªã³ã°ããŸãã XamlReaderã䜿çšããŠãçµæã®ããŒã¯ã¢ããã解æããäœæããã€ã³ã¹ã¿ã³ã¹ãFlowDocumentReaderã«é 眮ããŸãã ãã¹ãŠãç§ãã¡ã«åã£ãŠããå Žåã¯ããã®ããã¥ã¡ã³ãã®PDFãžã®å€æã«é²ã¿ãŸãã
PDFçæ
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { using (var stream = new FileStream("doc.xps", FileMode.Create)) { using (var package = Package.Open(stream, FileMode.Create, FileAccess.ReadWrite)) { using (var xpsDoc = new XpsDocument(package, CompressionOption.Maximum)) { var rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDoc), false); var paginator = ((IDocumentPaginatorSource)DocViewer.Document).DocumentPaginator; rsm.SaveAsXaml(paginator); rsm.Commit(); } } stream.Position = 0; var pdfXpsDoc = PdfSharp.Xps.XpsModel.XpsDocument.Open(stream); PdfSharp.Xps.XpsConverter.Convert(pdfXpsDoc, "doc.pdf", 0); } }
ãããŠãããã§ã¯ãã¹ãŠãç°¡åã§ãã XPSããã¥ã¡ã³ãããã±ãŒãžãçæãããŸãïŒãåãã®ãšãããXPSã¯å€ãã®XMLããã³ãã®ä»ã®ãªãœãŒã¹ãå«ãzipã¢ãŒã«ã€ãã§ãïŒã 以åã«ã¬ã³ããªã³ã°ãããFlowDocumentã¯ãäœæãããXPSããã±ãŒãžã«ä¿åãããŸãã ïŒéããåã«ïŒïŒXPSããã±ãŒãžã®ã¹ããªãŒã ã¯ãPDFSharpã䜿çšããŠXPSããã¥ã¡ã³ããããŠã³ããŒãããŠããŸãã ãã®åŸãããŠã³ããŒããããXPSã¯PDFã«å€æãããŸãã
ãããã«
çµè«ãšããŠããã®ã¢ãããŒãã§èªåã®ããã«ç¹å®ããå©ç¹ã®ãªã¹ããæäŸããããšæããŸãã
- ç¡æ-ç¡æã®ã©ã€ãã©ãªïŒMITïŒã®å©ããåããŠãéèŠãªããžãã¹äžã®åé¡ã®1ã€ã解決ããããšãã§ããŸãã
- 仲ä»ãšããŠã®FlowDocument-ããã¯ãããŒãžæ§é ã®ã»ãŒãã€ãã£ããµããŒãã§ãããããã¥ã¡ã³ãå ã§WPFã³ã³ãããŒã«ã䜿çšããæ©èœã§ãã
- ã¹ã¿ã€ãªã³ã°-FlowDocumentã®äœ¿çšã®ãããã§ãã¹ã¿ã€ã«ã䜿çšããŠWPFããã¥ã¡ã³ããã¹ã¿ã€ã«èšå®ã§ããŸã
- ã€ã³ã¿ã©ã¯ãã£ãæ§-ãªããªã WPFã³ã³ãããŒã«ã䜿çšããŠãPDFã®ãå°å·ãã®åã«ãå¿ èŠã«å¿ããŠãããã¥ã¡ã³ãå ã§ããã€ãã®å€æŽãšèšç®ãè¡ãããšãã§ããŸãã ãã®å ŽåãBindingã®äœ¿çšãå¯èœã§ãïŒããã«ã¯ããã€ãã®åé¡ããããŸãããDispatcherãBindingã®æŽæ°ãéå§ããã«ã¯ããã¯ãå¿ èŠã§ãïŒã
- ããžã¥ã¢ã«ãã¶ã€ããŒ-ãã³ãã¬ãŒããæºåãããšãã«ãéåžžã®Visual Studioãã¶ã€ããŒã䜿çšã§ããŸãã å¯äžã®äžæºã¯ããã©ãŒã "{{someProp}}"ã®DotLiquidãã€ã³ããŒãXAMLããŒã¯ã¢ãããšäºææ§ããªãããšã§ãã ã{}ãã®å é ã§æ¿å ¥ããã€ãã¹ã§ããŸãïŒ<TextBlock Text = " {} {{step.Title}}" ... />
ãæž èŽããããšãããããŸããïŒ