From 5e71baa6776d705e4a0aff610262f6eee48b753c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Erbsh=C3=A4u=C3=9Fer?= Date: Sat, 15 May 2021 15:30:09 +0200 Subject: [PATCH] Fixed printing problems on Windows 8. Since printing is broken on Windows 8, the XPS viewer will be opened directly. Fixed wrong dates in printed document if tenant entered or departed. --- ucalc.sln | 8 +- ucalc/Constants.cs | 6 +- ucalc/Controls/PrintableDocument.cs | 432 ++++++++++++++++++++++++---- ucalc/PrintWindow.xaml | 9 - ucalc/PrintWindow.xaml.cs | 244 +--------------- ucalc/ucalc.csproj | 1 + 6 files changed, 407 insertions(+), 293 deletions(-) diff --git a/ucalc.sln b/ucalc.sln index 334ef9b..b0bdd94 100644 --- a/ucalc.sln +++ b/ucalc.sln @@ -3,18 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30128.74 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ucalc", "ucalc\ucalc.csproj", "{E565705A-BAF4-4653-99A1-199C314A21EC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ucalc", "ucalc\ucalc.csproj", "{E565705A-BAF4-4653-99A1-199C314A21EC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|x64.ActiveCfg = Debug|x64 + {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|x64.Build.0 = Debug|x64 {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|Any CPU.Build.0 = Release|Any CPU + {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|x64.ActiveCfg = Release|x64 + {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ucalc/Constants.cs b/ucalc/Constants.cs index 5543894..01a525f 100644 --- a/ucalc/Constants.cs +++ b/ucalc/Constants.cs @@ -47,9 +47,9 @@ namespace UCalc var dinA4MarginLeftRight = (double) converter.ConvertFromInvariantString("3.18cm"); var dinA4MarginTopBottom = (double) converter.ConvertFromInvariantString("2.54cm"); - PrintDefaultFontSize = (double) converter.ConvertFromInvariantString("14pt"); - PrintSubjectFontSize = (double) converter.ConvertFromInvariantString("16pt"); - PrintNewlineFontSize = (double) converter.ConvertFromInvariantString("14pt"); + PrintDefaultFontSize = (double) converter.ConvertFromInvariantString("11pt"); + PrintSubjectFontSize = (double) converter.ConvertFromInvariantString("12pt"); + PrintNewlineFontSize = (double) converter.ConvertFromInvariantString("11pt"); // ReSharper restore PossibleNullReferenceException DinA4Padding = new Thickness(dinA4MarginLeftRight, dinA4MarginTopBottom, dinA4MarginLeftRight, diff --git a/ucalc/Controls/PrintableDocument.cs b/ucalc/Controls/PrintableDocument.cs index b8ed7c0..946f1d4 100644 --- a/ucalc/Controls/PrintableDocument.cs +++ b/ucalc/Controls/PrintableDocument.cs @@ -1,109 +1,443 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Packaging; using System.Linq; +using System.Printing; +using System.Windows; using System.Windows.Controls; using System.Windows.Documents; -using System.Windows.Markup; +using System.Windows.Media; using System.Windows.Xps.Packaging; using System.Windows.Xps.Serialization; +using Microsoft.Win32; +using UCalc.Data; namespace UCalc.Controls { + public class NoPrinterException : Exception + { + public NoPrinterException(string message) : base(message) + { + } + } + public class PrintableDocument : IDisposable { private static readonly Uri DocUri = new Uri($"pack://mietrechner{new Guid()}.xps"); private readonly Package _package; - private readonly XpsDocument _document; + private readonly FlowDocument _flowDocument; + private readonly FixedDocumentSequence _previewDocument; + private XpsDocument _fixedDocument; - private int PageCount => - _document.GetFixedDocumentSequence()!.References.First().GetDocument(false)!.Pages.Count; - - public PrintableDocument(IDocumentPaginatorSource source) + public PrintableDocument(Billing billing, IEnumerable tenants) { _package = Package.Open(new MemoryStream(), FileMode.Create, FileAccess.ReadWrite); - PackageStore.AddPackage(DocUri, _package); - _document = new XpsDocument(_package, CompressionOption.SuperFast, DocUri.AbsoluteUri); - var manager = new XpsSerializationManager(new XpsPackagingPolicy(_document), false); - - var paginator = source.DocumentPaginator; - manager.SaveAsXaml(paginator); + _flowDocument = CreateFlowDocument(billing, tenants); + _previewDocument = CreatePreview(_flowDocument); } public void Dispose() { - ((IDisposable) _document).Dispose(); - ((IDisposable) _package).Dispose(); + ((IDisposable) _fixedDocument).Dispose(); PackageStore.RemovePackage(DocUri); + ((IDisposable) _package).Dispose(); + } + + private static Size? GetPrinterMediaSize() + { + var localPrintServer = new LocalPrintServer(); + + var size = localPrintServer.GetPrintQueues() + .Select(printQueue => printQueue.DefaultPrintTicket?.PageMediaSize) + .FirstOrDefault(mediaSize => mediaSize is {PageMediaSizeName: PageMediaSizeName.ISOA4}); + if (size == null || !size.Width.HasValue || !size.Height.HasValue) + { + return null; + } + + return new Size(size.Width!.Value, size.Height!.Value); } public void PreviewIn(DocumentViewer viewer) { - viewer.Document = _document.GetFixedDocumentSequence(); + viewer.Document = _previewDocument; } public void Print(string description) { - var dialog = new PrintDialog {UserPageRangeEnabled = true, MinPage = 1, MaxPage = (uint) PageCount}; + var version = (int) Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + "CurrentMajorVersionNumber", 8); - if (dialog.ShowDialog() == true) + if (version == 10) { - var documentSequence = _document.GetFixedDocumentSequence(); - Package convPackage = null; - XpsDocument convDocument = null; + PrintDocumentImageableArea imageableArea = null; + var pageRangeSelection = PageRangeSelection.AllPages; + PageRange pageRange; - try + var writer = PrintQueue.CreateXpsDocumentWriter(description, ref imageableArea, ref pageRangeSelection, + ref pageRange); + if (writer == null) { - if (dialog.PageRangeSelection == PageRangeSelection.UserPages) + return; + } + + var clonedDocument = CloneFlowDocument(_flowDocument); + clonedDocument.PagePadding = Constants.DinA4Padding; + clonedDocument.ColumnWidth = double.PositiveInfinity; + + var paginator = ((IDocumentPaginatorSource) clonedDocument).DocumentPaginator; + paginator.PageSize = new Size(imageableArea.MediaSizeWidth, imageableArea.MediaSizeHeight); + + paginator.ComputePageCount(); + + if (pageRangeSelection == PageRangeSelection.UserPages) + { + if (pageRange.PageFrom > paginator.PageCount || pageRange.PageTo > paginator.PageCount || + pageRange.PageFrom > pageRange.PageTo) { - convDocument = ExtractPages(_document, dialog.PageRange.PageFrom - 1, - dialog.PageRange.PageTo - 1, out convPackage); - documentSequence = convDocument.GetFixedDocumentSequence(); + MessageBox.Show("Die eingegebenen Seitenzahlen sind ungültig!", "Ungültig!", + MessageBoxButton.OK, MessageBoxImage.Error); + return; } - dialog.PrintDocument(documentSequence!.DocumentPaginator, description); + paginator = new LimitedPaginator(paginator, pageRange.PageFrom - 1, pageRange.PageTo - 1); } - finally + + writer.Write(paginator); + } + else + { + var appDataPath = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/UCalc"; + var path = $"{appDataPath}\\print.xps"; + + if (File.Exists(path)) { - convDocument?.Close(); - convPackage?.Close(); + File.Delete(path); } + + var xpsDocument = new XpsDocument(path, FileAccess.ReadWrite); + using var manager = new XpsSerializationManager(new XpsPackagingPolicy(xpsDocument), false); + + var paginator = ((IDocumentPaginatorSource) _flowDocument).DocumentPaginator; + manager.SaveAsXaml(paginator); + + xpsDocument.Close(); + + var process = new Process(); + process.StartInfo = new ProcessStartInfo(path) + { + UseShellExecute = true + }; + process.Start(); } } - private static XpsDocument ExtractPages(XpsDocument source, int fromPage, int toPage, out Package package) + private FixedDocumentSequence CreatePreview(IDocumentPaginatorSource document) { - package = Package.Open(new MemoryStream(), FileMode.Create, FileAccess.ReadWrite); + _fixedDocument = new XpsDocument(_package, CompressionOption.SuperFast, DocUri.AbsoluteUri); + using var manager = new XpsSerializationManager(new XpsPackagingPolicy(_fixedDocument), false); - var docUri = new Uri("pack://mietrechnerTempTicket.xps"); - PackageStore.RemovePackage(docUri); - PackageStore.AddPackage(docUri, package); + var paginator = document.DocumentPaginator; + manager.SaveAsXaml(paginator); + return _fixedDocument.GetFixedDocumentSequence(); + } - var document = new XpsDocument(package, CompressionOption.SuperFast, docUri.AbsoluteUri); - var pages = source.GetFixedDocumentSequence()!.References.First().GetDocument(false)!.Pages; + private static FlowDocument CloneFlowDocument(FlowDocument document) + { + var sourceRange = new TextRange(document.ContentStart, document.ContentEnd); - var documentReference = new DocumentReference(); - var fixedDocument = new FixedDocument(); - documentReference.SetDocument(fixedDocument); + using var stream = new MemoryStream(); + sourceRange.Save(stream, DataFormats.Xaml); - for (var i = fromPage; i <= toPage; ++i) + var targetDocument = new FlowDocument(); + var targetRange = new TextRange(targetDocument.ContentStart, targetDocument.ContentEnd); + targetRange.Load(stream, DataFormats.Xaml); + + return targetDocument; + } + + private class LimitedPaginator : DocumentPaginator + { + private readonly DocumentPaginator _paginator; + private readonly int _minPage; + private readonly int _maxPage; + + public LimitedPaginator(DocumentPaginator paginator, int minPage, int maxPage) { - var page = pages[i]; - var pageContentChild = new PageContent {Source = page.Source}; - ((IUriContext) pageContentChild).BaseUri = ((IUriContext) page).BaseUri; - pageContentChild.GetPageRoot(false); - fixedDocument.Pages.Add(pageContentChild); + _paginator = paginator; + _minPage = minPage; + _maxPage = maxPage; } - var documentSequence = new FixedDocumentSequence(); - documentSequence.References.Add(documentReference); + public override DocumentPage GetPage(int pageNumber) + { + return _paginator.GetPage(_minPage + pageNumber); + } - var writer = XpsDocument.CreateXpsDocumentWriter(document); - writer.Write(documentSequence); + public override bool IsPageCountValid => _minPage < _paginator.PageCount && + _maxPage < _paginator.PageCount && + _minPage <= _maxPage; + + public override int PageCount => _maxPage - _minPage + 1; + + public override Size PageSize + { + get => _paginator.PageSize; + set => _paginator.PageSize = value; + } + + public override IDocumentPaginatorSource Source => _paginator.Source; + } + + private static FlowDocument CreateFlowDocument(Billing billing, IEnumerable tenants) + { + var size = GetPrinterMediaSize(); + if (!size.HasValue) + { + throw new NoPrinterException("Failed to query printer"); + } + + var document = new FlowDocument + { + PageWidth = size.Value.Width, + PageHeight = size.Value.Height, + PagePadding = Constants.DinA4Padding, + ColumnWidth = double.PositiveInfinity + }; + + foreach (var tenant in tenants) + { + var result = BillingCalculator.CalculateForTenant(billing, tenant); + AddPagesForTenant(document, billing, tenant, result); + } return document; } + + private static void AddLineBreaks(TableRowGroup rowGroup, int count) + { + var row = new TableRow(); + rowGroup.Rows.Add(row); + + var cell = new TableCell {ColumnSpan = 2}; + row.Cells.Add(cell); + cell.Blocks.Add(new Paragraph(new Run(new string('\n', count - 1)) + {FontSize = Constants.PrintNewlineFontSize})); + } + + private static void AddText(TableRowGroup rowGroup, string text, double? fontSize = null, + bool alignRight = false) + { + var row = new TableRow(); + rowGroup.Rows.Add(row); + + var cell = new TableCell {ColumnSpan = 2}; + row.Cells.Add(cell); + + if (alignRight) + { + cell.TextAlignment = TextAlignment.Right; + } + + var paragraph = new Paragraph(new Run(text)); + cell.Blocks.Add(paragraph); + + if (fontSize != null) + { + paragraph.FontSize = fontSize.Value; + } + } + + private static void AddPagesForTenant(FlowDocument document, Billing billing, Tenant tenant, + TenantCalculationResult result) + { + var section = new Section {BreakPageBefore = true, FontSize = Constants.PrintDefaultFontSize}; + document.Blocks.Add(section); + + var table = new Table {CellSpacing = 0}; + section.Blocks.Add(table); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + + var rowGroup = new TableRowGroup(); + table.RowGroups.Add(rowGroup); + + void AddCost(string name, decimal amount, bool isLast = false) + { + var row2 = new TableRow(); + rowGroup.Rows.Add(row2); + + if (isLast) + { + row2.Background = Brushes.LightGray; + } + + var cell2 = new TableCell + {BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1, 1, 0, isLast ? 1 : 0)}; + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run(name)) {Padding = new Thickness(6)}); + + cell2 = new TableCell + { + BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1, 1, 1, isLast ? 1 : 0), + TextAlignment = TextAlignment.Right + }; + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run($"{amount.CeilToString()} €")) {Padding = new Thickness(6)}); + } + + void AddTextLeftRight(string leftText, string rightText) + { + var row2 = new TableRow(); + rowGroup.Rows.Add(row2); + + var cell2 = new TableCell(); + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run(leftText))); + + cell2 = new TableCell + { + TextAlignment = TextAlignment.Right + }; + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run(rightText))); + } + + AddTextLeftRight( + $"{billing.Landlord.Name}\n" + + $"{billing.Landlord.Address.Street} {billing.Landlord.Address.HouseNumber}\n" + + $"{billing.Landlord.Address.Postcode} {billing.Landlord.Address.City}\n" + + $"Telefon: {billing.Landlord.Phone}" + + (string.IsNullOrEmpty(billing.Landlord.MailAddress) ? "" : $"\nEmail: {billing.Landlord.MailAddress}"), + DateTime.Now.ToString(Constants.DateFormat) + ); + + AddLineBreaks(rowGroup, 2); + + AddText( + rowGroup, + $"{tenant.Salutation.AsString()} {tenant.Name}\n" + + $"{billing.House.Address.Street} {billing.House.Address.HouseNumber}\n" + + $"{billing.House.Address.Postcode} {billing.House.Address.City}" + ); + + AddLineBreaks(rowGroup, 4); + + AddText(rowGroup, $"{tenant.Salutation.AsString()} {tenant.Name}"); + + AddLineBreaks(rowGroup, 1); + + var startDate = billing.StartDate; + if (tenant.EntryDate.HasValue && tenant.EntryDate.Value > startDate) + { + startDate = tenant.EntryDate.Value; + } + + var endDate = billing.EndDate; + if (tenant.DepartureDate.HasValue && tenant.DepartureDate.Value < endDate) + { + endDate = tenant.DepartureDate.Value; + } + + AddText( + rowGroup, + $"Nebenkostenabrechnung vom {startDate.ToString(Constants.DateFormat)} zum {endDate.ToString(Constants.DateFormat)}", + Constants.PrintSubjectFontSize + ); + + AddLineBreaks(rowGroup, 1); + + if (!string.IsNullOrEmpty(tenant.CustomMessage1)) + { + AddText(rowGroup, tenant.CustomMessage1); + + AddLineBreaks(rowGroup, 1); + } + + foreach (var (cost, costResult) in result.Costs) + { + AddCost(cost.Name, costResult.TotalAmount); + } + + AddCost("Zwischensumme", result.SubTotalAmount); + AddCost("Bereits gezahlt", tenant.PaidRent); + AddCost(result.TotalAmount > 0 ? "Einmalige Nachzahlung" : "Einmalige Rückzahlung", result.TotalAmount, + true); + + AddLineBreaks(rowGroup, 1); + + if (result.TotalAmount > 0) + { + AddText( + rowGroup, + $"Bitte überweisen Sie den einmaligen Betrag von {result.TotalAmount.CeilToString()} € auf das untenstehende Konto." + ); + } + else + { + AddText( + rowGroup, + $"Der einmalige Betrag von {(result.TotalAmount * -1).CeilToString()} € wird in den nächsten Tagen auf Ihr Konto überwiesen." + ); + } + + AddLineBreaks(rowGroup, 1); + + if (!string.IsNullOrEmpty(tenant.CustomMessage2)) + { + AddText(rowGroup, tenant.CustomMessage2); + AddLineBreaks(rowGroup, 1); + } + + AddText(rowGroup, "Kontoverbindung:"); + AddText(rowGroup, $"IBAN: {billing.Landlord.BankAccount.Iban}"); + AddText(rowGroup, $"BIC: {billing.Landlord.BankAccount.Bic}"); + AddText(rowGroup, $"Name der Bank: {billing.Landlord.BankAccount.BankName}"); + + AddLineBreaks(rowGroup, 2); + + AddText(rowGroup, "Mit freundlichen Grüßen"); + + if (result.Costs.Any(t => t.Key.DisplayInBill)) + { + AddPagesForTenantDetails(document, result); + } + } + + private static void AddPagesForTenantDetails(FlowDocument document, TenantCalculationResult result) + { + var section = new Section {BreakPageBefore = true, FontSize = Constants.PrintDefaultFontSize}; + document.Blocks.Add(section); + + var table = new Table {CellSpacing = 0}; + section.Blocks.Add(table); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + + var rowGroup = new TableRowGroup(); + table.RowGroups.Add(rowGroup); + + AddText(rowGroup, "Details zur Berechnung:"); + + AddLineBreaks(rowGroup, 1); + + foreach (var (cost, costResult) in result.Costs) + { + if (!cost.DisplayInBill) + { + continue; + } + + AddText(rowGroup, costResult.Details); + } + } } } \ No newline at end of file diff --git a/ucalc/PrintWindow.xaml b/ucalc/PrintWindow.xaml index 03dc0a5..826b694 100644 --- a/ucalc/PrintWindow.xaml +++ b/ucalc/PrintWindow.xaml @@ -17,15 +17,6 @@ Icon="logo.ico"> - - - -