标签归档:wpf

关于 WPF PrintDialog 设置打印纸张高度与宽度问题。

    • 问题如下:

      我通过WPF进行打印,WPF中PrintDialog默认纸张大小应该是A4,宽度是700多左右,高度是1000多。当我想要在X轴1000的位置打印文字时,总是打印不出来。因此我想通过设置打印纸张的大小来让X轴1000处的文字打印出来。代码如下。设置PageMediaSize为 ISOA0,应该是3000*4000多。但是我还是无法将X轴1000处的文字打印出来。网上搜了很久无果。因此在这里提问。希望能解决这个问题。代码如下。

      private void button_SinglePointPrint_Click(object sender, RoutedEventArgs e)
      {
      PrintDialog pDialog = new PrintDialog();
      pDialog.PrintTicket = pt;
      pDialog.PrintQueue = pq;
      pDialog.PrintTicket.PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA0);
      if (pDialog.ShowDialog() == true)
      {
      pt = pDialog.PrintTicket;
      pq = pDialog.PrintQueue;

      DrawingVisual vis = new DrawingVisual();
      DrawingContext dc = vis.RenderOpen();

      dc.DrawText(pLib.GetFormattedText(textBox_SinglePointText.Text, TextAlignment.Center, new Typeface(fontName), fontSize), new Point(1000,100));
      dc.Close();

      pDialog.PrintVisual(vis, “TestSinglePoint”);
      }
      }

      2014年4月21日 15:38
      kouxuelong 的头像

      0 分数

答案

  • 你好,

    你可以通过设置PrintTicket.PageMediaSizeSize,然后Transform你的窗体大小。请查看下面代码:

     private void _print()
     {
          PrintDialog printDlg = new System.Windows.Controls.PrintDialog();
    
           PrintTicket pt = printDlg.PrintTicket;
           Double printableWidth = pt.PageMediaSize.Width.Value;
           Double printableHeight = pt.PageMediaSize.Height.Value;
    
           Double xScale = (printableWidth - xMargin * 2) / printableWidth;
           Double yScale = (printableHeight - yMargin * 2) / printableHeight;
    
           this.Transform = new MatrixTransform(xScale, 0, 0, yScale, xMargin, yMargin);
    
        //now print the visual to printer to fit on the one page.
         printDlg.PrintVisual(this, "Print Page");
     }

    谢谢!

WPF小知识,MessageBox的多种用法

我们在程序中经常会用到MessageBox。

现将其常见用法总结如下:

1.MessageBox.Show(“Hello~~~~”);

最简单的,只显示提示信息。

2.MessageBox.Show(“There are something wrong!”,”ERROR”);

可以给消息框加上标题。

3.if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel) == DialogResult.OK)

{

//delete

}

询问是否删除时会用到这个。

4.if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel,MessageBoxIcon.Question) == DialogResult.OK)

{

//delete

}

可以给MessageBox加上一个Icon,.net提供常见的Icon共选择。

5.if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel, MessageBoxIcon.Question,MessageBoxDefaultButton.Button2) == DialogResult.OK)

{

//delete

}

可以改变MessageBox的默认焦点,如下:

6.if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel, MessageBoxIcon.Question,MessageBoxDefaultButton.Button2,MessageBoxOptions.RtlReading) == DialogResult.OK)

{

//delete

}

反向显示:

7.if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2, MessageBoxOptions.RightAlign,true) == DialogResult.OK)

{

//delete

}

添加Help按钮:

8.if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1, MessageBoxOptions.RtlReading, @”/folder/file.htm”) == DialogResult.OK)

{

//delete

}

指定帮助文件的路径,点击即可打开该路径下的帮助文件。

9.//HelpNavigator指定常数来指示要显示的帮助文件元素。Find 帮助文件将打开到搜索页。

if (MessageBox.Show(“Delete this user?”, “Confirm Message”, MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1, MessageBoxOptions.RtlReading, @”/folder/file.htm”, HelpNavigator.Find) == DialogResult.OK)

{

//delete

}

还有一些用法,不是太实用这里就不一一介绍了,有兴趣的朋友可以参考下这里:MSDN的MessageBox类。

========================================================================
【函数】 <整型> MessageBox(<字符串> Text, <字符串> Title, <整型> nType,MessageBoxIcon);
【函数说明】 弹出一个消息框。
【语法】
参数:
Text <字符串>,消息框的正文;
Title <字符串>,消息框的标题;
nType <整型>,消息框的类型。
返回值:<整型>,用户在消息框上点击关闭时的选择的按钮。 MessageBoxIcon:对话框上显示的图标样式。

【说明】
MessageBox(“消息内容”, “返回值 确定1”,MessageBoxButtons.OK,MessageBoxIcon.Question);
MessageBox(“消息内容”,, “返回值 确定1 取消2”,MessageBoxButtons.OKCancel, MessageBoxIcon.Asterisk);
MessageBox(“消息内容”, “返回值 终止3 重试4 忽略5”,MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Error);
MessageBox(“消息内容”, “返回值 是6 否7 取消2”,MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);
MessageBox(“消息内容”, “返回值 是6 否7”,MessageBoxButtons.YesNo, MessageBoxIcon.Hand);
MessageBox(“消息内容”, “返回值 重试4 取消2”,MessageBoxButtons.RetryCancel, MessageBoxIcon.Information);

MessageBoxIcon: 所有图标样式

MessageBoxIcon.Question MessageBoxIcon.Asterisk MessageBoxIcon.Information MessageBoxIcon.Error MessageBoxIcon.Stop MessageBoxIcon.Hand MessageBoxIcon.Exclamation MessageBoxIcon.Warning MessageBoxIcon.None

MessageBox函数MessageBox()函数MessageBox是标准的windows Api函数只能在CWnd类的继承类中使用,在C#中使用时,通常用MessageBox的show方法来实现对话框的弹出,命名空间System.Windows.Forms

应用实例:

DialogResult 是枚举类可以用枚举值直接比较MessageBox的返回值也可以转换为整型后再比较。如下:DialogResult r1 = MessageBox.Show ( “是否确定?” , “垃圾处理!” , MessageBoxButtons.AbortRetryIgnore , MessageBoxIcon.Question ) ;
int ss1=(int)r1 ;
if ( ss1==3 ){ }
if ( ss1==4 ){ }
if ( ss1==5){ }

或者是

if (DialogResult.Yes == MessageBox.Show(“232”, “”, MessageBoxButtons.YesNo, MessageBoxIcon.Information,MessageBoxDefaultButton.Button1))
{
MessageBox.Show(“122”);
}

WPF有一个 button, button 的背景是一张图片, 当我运行程序后, 点击button的时候, button会有按下的效果,但同时 背景图片也消失了, 等松开鼠标的时候,背景图片又回来了,

回复次数:4

WPF后台动态调用样式文件

应用场合:如果您的WPF应用程序设置WPF运行一个实例代码后,App.xaml文件中对样式资源字典文件的引用将失效.

解决办法1:在App.xaml.cs文件中用反射动态调用另外一个DLL项目中的样式文件即可

详细操作介绍如下:

1、WPF设置只运行一个实例代码:

App.xaml文件代码如下:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source=”ButtonStyle.xaml”/>
</ResourceDictionary>
</Application.Resources>
</Application>

App.xaml.cs文件代码如下:

//添加引用
using System.Diagnostics;
using System.IO;
using System.Reflection;

namespace WpfUI
{

/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{

public App()
{
}

/// <summary>
/// 要设置App.xaml的文件属性中生成操作=无
/// </summary>
[STAThread]
public static void Main()
{
App myApp = new App();
myApp.ShutdownMode = ShutdownMode.OnExplicitShutdown;
myApp.Run();
}

private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
if (e.Exception is InvalidOperationException)
e.Handled = true;
}

protected override void OnStartup(StartupEventArgs e)
{
//获取当前运行WPF程序的进程实例
Process process = Process.GetCurrentProcess();
//遍历WPF程序的同名进程组
foreach (Process p in Process.GetProcessesByName(process.ProcessName))
{
if (p.Id != process.Id && (p.StartTime – process.StartTime).TotalMilliseconds <= 0)
{
p.Kill();//关闭进程
return;
}
}
base.OnStartup(e);
//启动登陆窗体,

MainWindow myWindow = new MainWindow();

myWindow.Show();
}

}

}

2、ButtonStyle.xaml样式文件内容如下:

<ResourceDictionary xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml“>
<!–按钮样式–>
<Style x:Key=”RedButtonStyle” TargetType=”{x:Type Button}”>
<Setter Property=”Foreground” Value=”Red”/>
<Setter Property=”Background” Value=”Silver”/>
<Setter Property=”Height” Value=”23″/>
</Style>

</ResourceDictionary>

3、MainWindow.xaml文件内容如下:

<Window x:Class=”WpfUI.MainWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml
Title=”MainWindow” Height=”350″ Width=”525″>
<Grid>
<Button Content=”Button” Style=”{StaticResource RedButtonStyle}” Height=”23″ HorizontalAlignment=”Left” Margin=”102,66,0,0″ Name=”button1″ VerticalAlignment=”Top” Width=”75″ />
</Grid>
</Window>
4、运行程序后发现按钮样式RedButtonStyle总提示找不到

5、 解决办法如下:

第一步: 新建一个Windows–类库项目WpfThems,将ButtonStyle.xaml拷贝过去, 设置ButtonStyle.xaml文件的属性生成操作为 “Page”,之后生成WpfThems.dll文件

第二步:在当前项目WpfUI中添加WpfThems项目引用

第三步:修改App.xaml.cs 文件代码:添加一个函数LoadStyleResource并修改OnStartup函数内容。修改后的App.xaml.cs文件内容如下:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
//添加引用
using System.Diagnostics;
using System.IO;
using System.Reflection;

namespace WpfUI
{
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{

public App()
{
}

/// <summary>
/// 设置WpfUI项目中的App.xaml的文件属性中”生成操作”为”无”
/// 设置WpfThemes项目中的ButtonStyle.xaml的文件属性中”生成操作”为”Page”
/// </summary>
[STAThread]
public static void Main()
{
App myApp = new App();
myApp.ShutdownMode = ShutdownMode.OnExplicitShutdown;
myApp.Run();
}

/// <summary>
/// 重载应用程序启动函数
/// </summary>
/// <param name=”e”></param>
protected override void OnStartup(StartupEventArgs e)
{
//获取当前运行WPF程序的进程实例
Process process = Process.GetCurrentProcess();
//遍历WPF程序的同名进程组
foreach (Process p in Process.GetProcessesByName(process.ProcessName))
{
if (p.Id != process.Id && (p.StartTime – process.StartTime).TotalMilliseconds <= 0)
{
p.Kill();//关闭进程
return;
}
}
base.OnStartup(e);
//动态调用样式文件
LoadStyleResource();

//启动窗体
MainWindow myWindow = new MainWindow();
myWindow.Show();
}
private void LoadStyleResource()
{
Assembly assembly = Assembly.LoadFrom(“WpfThemes.dll”);
string packUri = @”/WpfThemes;component/ButtonStyle.xaml”;
ResourceDictionary myResourceDictionary = Application.LoadComponent(new Uri(packUri, UriKind.Relative)) as ResourceDictionary;
this.Resources.MergedDictionaries.Add(myResourceDictionary);
}
}
}

 

解决办法2:在每个窗体的xaml文件中添加对指定样式文件的引用

<Window.Resources>

<ResourceDictionary>

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source=”ButtonStyle.xaml”/>

</ResourceDictionary.MergedDictionaries>

</ResourceDictionary>

</Window.Resources>

详细工程项目请到我的下载资源中下载: http://download.csdn.net/detail/xqf222/5582575

WPF 登录窗口关闭时打开主窗口

在WPF中设计登录窗口关闭时打开主窗口,自动生成的App.xaml不能满足要求,

1、把App.xaml的属性窗口中的生成操作设定为 无

2、添加Program类

复制代码
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            LoginWindow loginForm = new LoginWindow();
            loginForm.Init();
            bool? rt = loginForm.ShowDialog();
            loginForm.Close();
            if (rt == true)
            {
                Application App = new Application();
                App.ShutdownMode = ShutdownMode.OnMainWindowClose;
                MainWindow m_MianWindow = new MainWindow();
                App.MainWindow = m_MianWindow;
                App.Run(m_MianWindow);
            }
        }
    }
复制代码

这样就可以满足要求了

参考:http://www.mysjtu.com/page/M0/S613/613036.html

 

方法二:

最近看一方法,不用添加Program方法即可,代码如下:

复制代码
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            Application.Current.ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;
            LoginWindow window = new LoginWindow();
            bool? dialogResult = window.ShowDialog();
            if ((dialogResult.HasValue == true) &&
                (dialogResult.Value == true))
            {
                base.OnStartup(e);
                Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
            }
            else
            {
                this.Shutdown();
            }
        }
    }
复制代码

通过Application的ShutdownMode控制进程的运行时间。

WPF 后台任务 等待动画 样例

运行效果:

前台代码:

[csharp] view plain copy

  1. <Window x :Class=“Waiting.Window1”
  2.         xmlns=“http://schemas.microsoft.com/winfx/2006/xaml/presentation”
  3.         xmlns:x =“http://schemas.microsoft.com/winfx/2006/xaml”
  4.         Title=“后台忙” Height=“94.635” Width=“197.361”>
  5.     <Grid >
  6.         <Button Content =“开始” HorizontalAlignment=“Left” Margin=“10,10,0,0” Name =“button1” VerticalAlignment=“Top” Click=“button1_Click” Width =“48” />
  7.         <Label Name =“lab_pro” Content=“” Height=“25” VerticalAlignment =“Top” HorizontalAlignment=“Left” Margin=“80,20,0,0” />
  8.         <!–动画代码,只要填写name属性即可–>
  9.         <Grid Name =“loading” Visibility=“Collapsed” Height=“41” Grid.Row =“0” VerticalAlignment=“Top” Margin=“126,10,0,0” HorizontalAlignment=“Left” Width =“42”>
  10.             <Grid.Resources>
  11.                 <DrawingBrush x :Key=“brush” Stretch=“None” AlignmentX =“Center” AlignmentY=“Top”>
  12.                     <DrawingBrush.Drawing>
  13.                         <GeometryDrawing Brush =“Black”>
  14.                             <GeometryDrawing.Geometry>
  15.                                 <EllipseGeometry RadiusX =“2” RadiusY=“5”/>
  16.                             </GeometryDrawing.Geometry>
  17.                         </GeometryDrawing>
  18.                     </DrawingBrush.Drawing>
  19.                 </DrawingBrush>
  20.             </Grid.Resources>
  21.             <Rectangle x :Name=“r01” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  22.                 <Rectangle.RenderTransform>
  23.                     <RotateTransform Angle =“0”/>
  24.                 </Rectangle.RenderTransform>
  25.             </Rectangle>
  26.             <Rectangle x :Name=“r02” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  27.                 <Rectangle.RenderTransform>
  28.                     <RotateTransform Angle =“30”/>
  29.                 </Rectangle.RenderTransform>
  30.             </Rectangle>
  31.             <Rectangle x :Name=“r03” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  32.                 <Rectangle.RenderTransform>
  33.                     <RotateTransform Angle =“60”/>
  34.                 </Rectangle.RenderTransform>
  35.             </Rectangle>
  36.             <Rectangle x :Name=“r04” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  37.                 <Rectangle.RenderTransform>
  38.                     <RotateTransform Angle =“90”/>
  39.                 </Rectangle.RenderTransform>
  40.             </Rectangle>
  41.             <Rectangle x :Name=“r05” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  42.                 <Rectangle.RenderTransform>
  43.                     <RotateTransform Angle =“120”/>
  44.                 </Rectangle.RenderTransform>
  45.             </Rectangle>
  46.             <Rectangle x :Name=“r06” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  47.                 <Rectangle.RenderTransform>
  48.                     <RotateTransform Angle =“150”/>
  49.                 </Rectangle.RenderTransform>
  50.             </Rectangle>
  51.             <Rectangle x :Name=“r07” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  52.                 <Rectangle.RenderTransform>
  53.                     <RotateTransform Angle =“180”/>
  54.                 </Rectangle.RenderTransform>
  55.             </Rectangle>
  56.             <Rectangle x :Name=“r08” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  57.                 <Rectangle.RenderTransform>
  58.                     <RotateTransform Angle =“210”/>
  59.                 </Rectangle.RenderTransform>
  60.             </Rectangle>
  61.             <Rectangle x :Name=“r09” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  62.                 <Rectangle.RenderTransform>
  63.                     <RotateTransform Angle =“240”/>
  64.                 </Rectangle.RenderTransform>
  65.             </Rectangle>
  66.             <Rectangle x :Name=“r10” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  67.                 <Rectangle.RenderTransform>
  68.                     <RotateTransform Angle =“270”/>
  69.                 </Rectangle.RenderTransform>
  70.             </Rectangle>
  71.             <Rectangle x :Name=“r11” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  72.                 <Rectangle.RenderTransform>
  73.                     <RotateTransform Angle =“300”/>
  74.                 </Rectangle.RenderTransform>
  75.             </Rectangle>
  76.             <Rectangle x :Name=“r12” Fill=“{StaticResource brush}” Opacity =“0.5” RenderTransformOrigin=“0.5,0.5”>
  77.                 <Rectangle.RenderTransform>
  78.                     <RotateTransform Angle =“330”/>
  79.                 </Rectangle.RenderTransform>
  80.             </Rectangle>
  81.             <Grid.Triggers>
  82.                 <EventTrigger RoutedEvent =“Grid.Loaded”>
  83.                     <BeginStoryboard>
  84.                         <Storyboard RepeatBehavior =“Forever”>
  85.                             <DoubleAnimation Storyboard.TargetName =“r01” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.00000” To=“0”/>
  86.                             <DoubleAnimation Storyboard.TargetName =“r02” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.08333” To=“0”/>
  87.                             <DoubleAnimation Storyboard.TargetName =“r03” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.16666” To=“0”/>
  88.                             <DoubleAnimation Storyboard.TargetName =“r04” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.24999” To=“0”/>
  89.                             <DoubleAnimation Storyboard.TargetName =“r05” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.33332” To=“0”/>
  90.                             <DoubleAnimation Storyboard.TargetName =“r06” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.41665” To=“0”/>
  91.                             <DoubleAnimation Storyboard.TargetName =“r07” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.49998” To=“0”/>
  92.                             <DoubleAnimation Storyboard.TargetName =“r08” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.58331” To=“0”/>
  93.                             <DoubleAnimation Storyboard.TargetName =“r09” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.66664” To=“0”/>
  94.                             <DoubleAnimation Storyboard.TargetName =“r10” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.74997” To=“0”/>
  95.                             <DoubleAnimation Storyboard.TargetName =“r11” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.83330” To=“0”/>
  96.                             <DoubleAnimation Storyboard.TargetName =“r12” Storyboard.TargetProperty=“Opacity” AutoReverse=“True” Duration=“0:0:0.08333” BeginTime =“0:0:0.91663” To=“0”/>
  97.                         </Storyboard>
  98.                     </BeginStoryboard>
  99.                 </EventTrigger>
  100.             </Grid.Triggers>
  101.         </Grid>
  102.     </Grid >
  103. </Window>

后台代码:

[csharp] view plain copy

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Data;
  9. using System.Windows.Documents;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using System.Windows.Media.Imaging;
  13. using System.Windows.Shapes;
  14. namespace Waiting
  15. {
  16.     /// <summary>
  17.     /// Window1.xaml 的交互逻辑
  18.     /// </summary>
  19.     public partial class Window1 : Window
  20.     {
  21.         public Window1()
  22.         {
  23.             InitializeComponent();
  24.         }
  25.         BackgroundWorker bgMeet;
  26.         private void button1_Click(object sender, RoutedEventArgs e)
  27.         {
  28.             bgMeet = new BackgroundWorker ();
  29.             //能否报告进度更新
  30.             bgMeet.WorkerReportsProgress = true;
  31.             //要执行的后台任务
  32.             bgMeet.DoWork += new DoWorkEventHandler (bgMeet_DoWork);
  33.             //进度报告方法
  34.             bgMeet.ProgressChanged += new ProgressChangedEventHandler (bgMeet_ProgressChanged);
  35.             //后台任务执行完成时调用的方法
  36.             bgMeet.RunWorkerCompleted += new RunWorkerCompletedEventHandler (bgMeet_RunWorkerCompleted);
  37.             bgMeet.RunWorkerAsync(); //任务启动
  38.         }
  39.         //执行任务
  40.         void bgMeet_DoWork(object sender, DoWorkEventArgs e)
  41.         {
  42.             //开始播放等待动画
  43.             this.Dispatcher.Invoke(new Action(() =>
  44.             {
  45.                 loading.Visibility = System.Windows. Visibility.Visible;
  46.             }));
  47.             //开始后台任务
  48.             GetData();
  49.         }
  50.         //报告任务进度
  51.         void bgMeet_ProgressChanged(object sender, ProgressChangedEventArgs e)
  52.         {
  53.             this.Dispatcher.Invoke(new Action(() =>
  54.             {
  55.                 this.lab_pro.Content = e.ProgressPercentage + “%”;
  56.             }));
  57.         }
  58.         //任务执行完成后更新状态
  59.         void bgMeet_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  60.         {
  61.             loading.Visibility = System.Windows. Visibility.Collapsed;
  62.             this.Dispatcher.Invoke(new Action(() =>
  63.             {
  64.                 this.lab_pro.Content = “完成” ;
  65.             }));
  66.         }
  67.         //模拟耗时任务
  68.         public void GetData()
  69.         {
  70.             for (int i = 0; i < 6; i++)
  71.             {
  72.                 bgMeet.ReportProgress(20*i);
  73.                 System.Threading. Thread.Sleep(400);
  74.             }
  75.         }
  76.     }
  77. }

WPF 设置TextBox为空时,背景为文字提示。

<TextBox FontSize="17" Height="26" Margin="230,150,189,0" Name="txt_Account" VerticalAlignment="Top" Foreground="Indigo" TabIndex="0" BorderThickness="1">
            <TextBox.Resources>
                <VisualBrush x:Key="HelpBrush" TileMode="None" Opacity="0.3" Stretch="None" AlignmentX="Left">
                    <VisualBrush.Visual>
                        <TextBlock FontStyle="Italic" Text="请输入用户名"/>
                    </VisualBrush.Visual>
                </VisualBrush>
            </TextBox.Resources>
            <TextBox.Style>
                <Style TargetType="TextBox">
                    <Style.Triggers>
                        <Trigger Property="Text" Value="{x:Null}">
                            <Setter Property="Background" Value="{StaticResource HelpBrush}"/>
                        </Trigger>
                        <Trigger Property="Text" Value="">
                            <Setter Property="Background" Value="{StaticResource HelpBrush}"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>

WPF中解决内存泄露的几点提示与解决方法

一直以来用WPF做一个项目,但是开发中途发现内存开销太大,用ANTS Memory Profiler分析时,发现在来回点几次载入页面的操作中,使得非托管内存部分开销从起始的43.59M一直到150M,而托管部分的开销也一直持高不下,即每次申请的内存在结束后不能完全释放。在网上找了不少资料,甚受益,现在修改后,再也不会出现这种现象了(或者说,即使有也不吓人),写下几个小心得:

1. 慎用WPF样式模板合并

我发现不采用合并时,非托管内存占用率较小,只是代码的理解能力较差了,不过我们还有文档大纲可以维护。

2. WPF样式模板请共享

共享的方式最简单不过的就是建立一个类库项目,把样式、图片、笔刷什么的,都扔进去,样式引用最好使用StaticResource,开销最小,但这样就导致了一些写作时的麻烦,即未定义样式,就不能引用样式,哪怕定义在后,引用在前都不行。

3. 慎用隐式类型var的弱引用

这个本来应该感觉没什么问题的,可是不明的是,在实践中,发现大量采用var与老老实实的使用类型声明的弱引用对比,总是产生一些不能正确回收的WeakRefrense(这点有待探讨,因为开销不是很大,可能存在一些手工编程的问题)

4. 写一个接口约束一下

谁申请谁释放,基本上这点能保证的话,内存基本上就能释放干净了。我是这么做的:

复制代码
    interface IUIElement : IDisposable
    {
        /// <summary>
        /// 注册事件
        /// </summary>
        void EventsRegistion();

        /// <summary>
        /// 解除事件注册
        /// </summary>
        void EventDeregistration();
    }
复制代码

在实现上可以这样:

复制代码
 1 #region IUIElement 成员
 2 public void EventsRegistion()
 3 {
 4     this.traineeReport.SelectionChanged += new SelectionChangedEventHandler(traineeReport_SelectionChanged);
 5 }
 6 
 7 public void EventDeregistration()
 8 {
 9     this.traineeReport.SelectionChanged -= new SelectionChangedEventHandler(traineeReport_SelectionChanged);
10 }
11 
12 private bool disposed;
13 
14 ~TraineePaymentMgr()
15 {
16     ConsoleEx.Log("{0}被销毁", this);
17     Dispose(false);
18 }
19 
20 public void Dispose()
21 {
22     ConsoleEx.Log("{0}被手动销毁", this);
23     Dispose(true);
24     GC.SuppressFinalize(this);
25 }
26 
27 protected void Dispose(bool disposing)
28 {
29     ConsoleEx.Log("{0}被自动销毁", this);
30     if(!disposed)
31     {
32         if(disposing)
33         {
34             //托管资源释放
35             ((IDisposable)traineeReport).Dispose();
36             ((IDisposable)traineePayment).Dispose();
37         }
38         //非托管资源释放
39     }
40     disposed = true;
41 }
42 #endregion
复制代码

比如写一个UserControl或是一个Page时,可以参考以上代码,实现这样接口,有利于资源释放。

5. 定时回收垃圾

复制代码
DispatcherTimer GCTimer = new DispatcherTimer();
public MainWindow()
{
    InitializeComponent();
    this.GCTimer.Interval = TimeSpan.FromMinutes(10); //垃圾释放定时器 我定为每十分钟释放一次,大家可根据需要修改
  this.GCTimer.start();

    this.EventsRegistion();    // 注册事件
}

public void EventsRegistion()
{
    this.GCTimer.Tick += new EventHandler(OnGarbageCollection);
}

public void EventDeregistration()
{
    this.GCTimer.Tick -= new EventHandler(OnGarbageCollection);
}

void OnGarbageCollection(object sender, EventArgs e)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}
复制代码

6. 较简单或可循环平铺的图片用GeometryDrawing实现

一个图片跟几行代码相比,哪个开销更少肯定不用多说了,而且这几行代码还可以BaseOn进行重用。

复制代码
<DrawingGroup x:Key="Diagonal_50px">
    <DrawingGroup.Children>
        <GeometryDrawing Brush="#FF2A2A2A" Geometry="F1 M 0,0L 50,0L 50,50L 0,50 Z"/>
        <GeometryDrawing Brush="#FF262626" Geometry="F1 M 50,0L 0,50L 0,25L 25,0L 50,0 Z"/>
        <GeometryDrawing Brush="#FF262626" Geometry="F1 M 50,25L 50,50L 25,50L 50,25 Z"/>
    </DrawingGroup.Children>
</DrawingGroup>
复制代码

这边是重用

<DrawingBrush x:Key="FrameListMenuArea_Brush" Stretch="Fill" TileMode="Tile" Viewport="0,0,50,50" ViewportUnits="Absolute"
Drawing="{StaticResource Diagonal_50px}"/>

上面几行代码相当于这个:

7. 使用Blend做样式的时候,一定要检查完成的代码

众所周知,Blend定义样式时,产生的垃圾代码还是比较多的,如果使用Blend,一定要检查生成的代码。

 

8. 静态方法返回诸如List<>等变量的,请使用out

比如

public static List<String> myMothod()

{...}

请改成

public static myMothod(out List<String> result)

{...}

 

9. 打针对此问题的微软补丁

3.5的应该都有了吧,这里附上NET4的内存泄露补丁地址,下载点这里 (QFE:  Hotfix request to implement hotfix KB981107 in .NET 4.0 )

这是官方给的说明,看来在样式和数据绑定部分下了点工夫啊:

  1. 运行一个包含样式或模板,请参阅通过使用 StaticResource 标记扩展或 DynamicResource 标记扩展应用程序资源的 WPF 应用程序。 创建使用这些样式或模板的多个控件。 但是,这些控件不使用引用的资源。 在这种情况的一些内存WeakReference对象和空间泄漏的控股数组后,垃圾回收释放该控件。
  2. 运行一个包含的控件的属性是数据绑定到的 WPF 应用程序DependencyObject对象。 该对象的生存期是超过控件的生存期。 许多控件时创建,一些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。
  3. 运行使用树视图控件或控件派生于的 WPF 应用程序,选择器类。 将控件注册为控制中的键盘焦点的内部通知在KeyboardNavigation类。 该应用程序创建这些控件的很多。 例如对于您添加并删除这些控件。 在本例中为某些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。

继续更新有关的三个8月补丁,详细的请百度:KB2487367  KB2539634  KB2539636,都是NET4的补丁,在发布程序的时候,把这些补丁全给客户安装了会好的多。

10.  对string怎么使用的建议

这个要解释话就长了,下面仅给个例子说明一下,具体的大家去找找MSDN

复制代码
        string ConcatString(params string[] items)
        {
            string result = "";
            foreach (string item in items)
            {
                result += item;
            }
            return result;
        }

        string ConcatString2(params string[] items)
        {
            StringBuilder result = new StringBuilder();
            for(int i=0, count = items.Count(); i<count; i++)
            {
                result.Append(items[i]);
            }
            return result.ToString();
        }
复制代码

建议在需要对string进行多次更改时(循环赋值、连接之类的),使用StringBuilder。我已经把工程里这种频繁且大量改动string的操作全部换成了StringBuilder了,用ANTS Memory Profiler分析效果显著,不仅提升了性能,而且垃圾也少了。

 

11. 其它用上的技术暂时还没想到,再补充…

 

如果严格按以上操作进行的话,可以得到一个满意的结果:

运行了三十分钟,不断的切换功能,然后休息5分钟,回头一看,结果才17M左右内存开销,效果显著吧。

然后对于调试信息的输出,我的做法是在窗体应用程序中附带一个控制台窗口,输出调试信息,给一个类,方便大家:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace Trainee.UI.UIHelper
{
    public struct COORD
    {
        public ushort X;
        public ushort Y;
    };

    public struct CONSOLE_FONT
    {
        public uint index;
        public COORD dim;
    };

    public static class ConsoleEx
    {
        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern bool AllocConsole();

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern bool SetConsoleFont(IntPtr consoleFont, uint index);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern bool GetConsoleFontInfo(IntPtr hOutput, byte bMaximize, uint count, [In, Out] CONSOLE_FONT[] consoleFont);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern uint GetNumberOfConsoleFonts();

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern COORD GetConsoleFontSize(IntPtr HANDLE, uint DWORD);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32.dll ")]
        internal static extern IntPtr GetStdHandle(int nStdHandle);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static extern int GetConsoleTitle(String sb, int capacity);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("user32.dll", EntryPoint = "UpdateWindow")]
        internal static extern int UpdateWindow(IntPtr hwnd);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("user32.dll")]
        internal static extern IntPtr FindWindow(String sClassName, String sAppName);

        public static void OpenConsole()
        {
            var consoleTitle = "> Debug Console";
            AllocConsole();


            Console.BackgroundColor = ConsoleColor.Black;
            Console.ForegroundColor = ConsoleColor.Cyan;
            Console.WindowWidth = 80;
            Console.CursorVisible = false;
            Console.Title = consoleTitle;
            Console.WriteLine("DEBUG CONSOLE WAIT OUTPUTING...{0} {1}\n", DateTime.Now.ToLongTimeString());

            try
            {
                //这里是改控制台字体大小的,可能会导致异常,在我这个项目中我懒得弄了,如果需要的的话把注释去掉就行了
                //IntPtr hwnd = FindWindow(null, consoleTitle);
                //IntPtr hOut = GetStdHandle(-11);

                //const uint MAX_FONTS = 40;
                //uint num_fonts = GetNumberOfConsoleFonts();
                //if (num_fonts > MAX_FONTS) num_fonts = MAX_FONTS;
                //CONSOLE_FONT[] fonts = new CONSOLE_FONT[MAX_FONTS];
                //GetConsoleFontInfo(hOut, 0, num_fonts, fonts);
                //for (var n = 7; n < num_fonts; ++n)
                //{
                //    //fonts[n].dim = GetConsoleFontSize(hOut, fonts[n].index);
                //    //if (fonts[n].dim.X == 106 && fonts[n].dim.Y == 33)
                //    //{
                //        SetConsoleFont(hOut, fonts[n].index);
                //        UpdateWindow(hwnd);
                //        return;
                //    //}
                //}
            }
            catch
            {

            }
        }

        public static void Log(String format, params object[] args)
        {
            Console.WriteLine("[" + DateTime.Now.ToLongTimeString() + "] " + format, args);
        }
        public static void Log(Object arg)
        {
            Console.WriteLine(arg);
        }
    }
}
复制代码

在程序启动时,可以用ConsoleEx.OpenConsole()打开控制台,用ConsoleEx.Log(…..)或者干脆用Console.WriteLine进行输出就可以了。

WPF的TextBox产生内存泄露的情况

前段时间参与了一个WPF编写的项目,在该项目中有这样一个场景:在程序运行过程中需要动态地产生大量文本信息,并追加WPF界面上的一个TextBox的Text中进行显示。编写完之后,运行该项目的程序,发现在产生大量信息之后,发现系统变慢了,打开任务管理器才发现,该项目的程序占用了将近1.5G的内存(天啊!!!这不是一般的耗内存啊!!!)。后来通过查资料和探索才发现了WPF的TextBox在追加Text显示文本时会造成内存泄露。下面通过一个小Demo程序来展示一下这个内存泄露。

我的Demo程序很简单,就是在界面上显示一个TextBox和一个Button,点击Button后就从0到9999进行for循环并将这些数字追加的TextBox的Text中进行显示。代码如下,

<window x:Class="TextBoxMemoryLeak.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="测试TextBox内存泄露" Height="350" Width="525"
        WindowStartupLocation="CenterScreen">
    <grid Margin="5">
        </grid><grid .RowDefinitions>
            <rowdefinition Height="*"></rowdefinition>
            <rowdefinition Height="35"></rowdefinition>
        </grid>
        <dockpanel Grid.Row="0">
            <textbox Name="tbOutput" IsReadOnly="True" VerticalScrollBarVisibility="Auto"></textbox>
        </dockpanel>
        <stackpanel Grid.Row="1"
                    FlowDirection="RightToLeft"
                    Orientation="Horizontal">
            <button Name="btnStart" Content="开 始" Margin="5,4,5,4" Width="65" Click="btnStart_Click"></button>
        </stackpanel>
    
</window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TextBoxMemoryLeak
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            this.btnStart.IsEnabled = false;
            this.tbOutput.Text = "";

            for (int i = 0; i < 10000; i++)
            {
                //使用此语句进行Textbox的追加会造成内存泄露
                //this.tbOutput.Text += string.Format("{0}\n", i);

                //使用此语句进行Textbox的追加可避免内存泄露
                this.tbOutput.AppendText(string.Format("{0}\n", i));
            }

            this.btnStart.IsEnabled = true;
        }
    }
}

界面如下所示:

内存泄露情况

最初我们采用的是TextBox的Text追加方式如下

this.tbOutput.Text += string.Format("{0}\n", i);

构建,启动调试后,我们查看任务管理器,此时所占内存只有16M,

点击【开始】按钮之后,等到从0输出到9999之后,我们再查看任务管理器,发现此时所占的内存飙到了600+M,

若此时再点击【开始】按钮,等循环结束,发现所占内存飙到了900+M,

再点击【开始】按钮的话,就要发生OutOfMemory异常的。当我们将循环改为从0到19999时,第一次点击【开始】按钮,我的机器就发生OutOfMemory异常了。

避免内存泄露的情况

将TextBox的Text追加方式改为下面语句

this.tbOutput.AppendText(string.Format("{0}\n", i));

构建,启动调试,然后点击界面的【开始】按钮,等循环结束,我们查看任务管理器,测试Demo程序只占了29M内存(此时是从0到19999的循环)。

 

 

 

TextBox存在内存泄露的可能

背景

-WPF桌面程序中,增加了一个TextBox控件,用于显示输出的日志信息,日志信息量很大(具体数值未统计)

XAML 代码
 <TextBox Text="{Binding Message}" TextWrapping="Wrap" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" TextChanged="TextBox_TextChanged" />

CS 代码
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (!(sender is TextBox)) return;
        var tb = (TextBox)sender;
        if (tb.Text.Length > 30000) //textbox maxlength = 32767
      {
        tb.Text = "Auto Clear Histroy Information\n\r";
      }
       tb.Select(tb.Text.Length, 0);
    }

问题

运行一段时间后操作系统弹出信息,称“XXX程序出问题了”(截图后补吧)。在增加日志显示功能前,此异常未出现。

解决

参考:Google搜索到的信息
怀疑是日志信息太多,造成程序内存泄露。TextBox控件支持回退功能(Undo),但是回退功能需要占用更多内存。

设置 
     UndoLimit="0"  或者 IsUndoEnabled="False"
 可关闭Undo功能

WPF – 简单异步模式

以WeatherForecast为例. 需求: 用户在窗体上点击一个按钮, 程序去网络上查询天气情况, 并把结果显示在窗体上. 网络查询是一个耗时任务, 在等待结果的同时, 用户将看到一个旋转的时钟动画表示程序正在查询.

 

模式为:

  1. 窗口类MainWindow中有耗时函数: string FetchWeatherFromInternet().
  2. 窗口类MainWindow中包含一个函数 UpdateUIWhenWeatherFetched(string result), 用于把任务结果显示在界面上.
  3. 当用户点击按钮时, 在 btnFetchWeather_Click() 中,

    如果是同步调用, 代码很简单, 如下:
    string result = this.FetchWeatherFromInternet();
    this.UpdateUserInterface( result );

    现在需要异步执行, 稍微麻烦点, 需要用到3个delegate, 其中一个代表要执行的任务, 另一个代表任务结束后的callback, 还有一个代表交给UI执行的任务, 上述代码被替换成如下:

    // 代表要执行的异步任务
    Func<string> asyncAction = this.FetchWeatherFromInternet();

    // 处理异步任务的结果
    Action<IAsyncResult> resultHandler = delegate( IAsyncResult asyncResult )
    {
    //获得异步任务的返回值, 这段代码必须在UI线程中执行
    string weather = asyncAction.EndInvoke( asyncResult );
    this.UpdateUIWhenWeatherFetched( weather );
    };

    //代表异步任务完成后的callback
    AsyncCallback asyncActionCallback = delegate( IAsyncResult asyncResult )
    {
    this.Dispatcher.BeginInvoke( DispatcherPriority.Background, resultHandler, asyncResult );
    };

    //这是才开始执行异步任务
    asyncAction.BeginInvoke( asyncActionCallback, null );

=======================================================================

private void ForecastButtonHandler(object sender, RoutedEventArgs e)
{
    this.UpdateUIWhenStartFetchingWeather();

    //异步任务封装在一个delegate中, 此delegate将运行在后台线程 
    Func<string> asyncAction = this.FetchWeatherFromInternet;

//在UI线程中得到异步任务的返回值,并更新UI
//必须在UI线程中执行

    Action<IAsyncResult> resultHandler =

delegate(IAsyncResult asyncResult)
{
string weather = asyncAction.EndInvoke(asyncResult);
this.UpdateUIWhenWeatherFetched(weather);
};


    //异步任务执行完毕后的callback, 此callback运行在后台线程上. 
    //此callback会异步调用resultHandler来处理异步任务的返回值.
    AsyncCallback asyncActionCallback = delegate(IAsyncResult asyncResult)
    {
        this.Dispatcher.BeginInvoke(DispatcherPriority.Background, resultHandler, asyncResult);
    };

    //在UI线程中开始异步任务, 
    //asyncAction(后台线程), asyncActionCallback(后台线程)和resultHandler(UI线程)
    //将被依次执行
    asyncAction.BeginInvoke(asyncActionCallback, null);
}

private string FetchWeatherFromInternet()
{
    // Simulate the delay from network access.
    Thread.Sleep(4000);
    String weather = "rainy";
    return weather;
}

private void UpdateUIWhenStartFetchingWeather()
{
    // Change the status
    this.fetchButton.IsEnabled = false;
    this.weatherText.Text = "";
}

private void UpdateUIWhenWeatherFetched(string weather)
{
    //Update UI text
    this.fetchButton.IsEnabled = true;
    this.weatherText.Text = weather;
}

在WPF的DataGrid中对行添加单击事件

在做的一个c#的项目中发现Datagrid没办法直接对鼠标单击进行响应,调用MouseDown事件也需要点击某一行第二次才能响应。所以借助EventSetter来简单的实现了一个。

界面部分的代码

        <DataGrid x:Name="dataGrid" HorizontalAlignment="Left" Margin="10,38,0,0"
                  VerticalAlignment="Top" Height="257" FontSize="13.333"
                  AutoGenerateColumns="False" AlternationCount="2" CanUserAddRows="False"
                  MinWidth="504" Width="513" SelectionUnit="FullRow">


            <DataGrid.RowStyle>
                <Style  TargetType="DataGridRow">
                    <EventSetter Event="GotFocus" Handler="Item_GotFocus"/>
                </Style>
            </DataGrid.RowStyle>
            <DataGrid.Columns>
                <DataGridCheckBoxColumn />
                <DataGridTextColumn Header="歌曲名" Width="200" Binding="{Binding Path=Title}" IsReadOnly="True"/>
                <DataGridTextColumn Header="艺术家" Width="127" Binding="{Binding Path=Artist}" IsReadOnly="True"/>
                <DataGridTextColumn Header="专辑" Width="140" Binding="{Binding Path=Album}" IsReadOnly="True"/>
            </DataGrid.Columns>
        </DataGrid>
  • 对应的c#的代码
        private void Item_GotFocus(object sender, RoutedEventArgs e)
        {
            var item = (DataGridRow)sender;
            FrameworkElement objElement = dataGrid.Columns[0].GetCellContent(item);
            if (objElement != null)
            {
                CheckBox objChk = (CheckBox)objElement;
                objChk.IsChecked = !objChk.IsChecked;
            }
        }

附上用mousedow事件的代码

        private void DataGrid_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                var se = dataGrid.SelectedItem;
                FrameworkElement objElement = dataGrid.Columns[0].GetCellContent(se);
                if (objElement != null)
                {
                    CheckBox objChk = (CheckBox)objElement;
                    objChk.IsChecked = !objChk.IsChecked;
                }
            }
        }
  • 需要在这个界面的构造函数中添加
 dataGrid.MouseDown += DataGrid_MouseDown;
  • 再附上一个效果图

这里写图片描述

1

用WPF实现打印及打印预览

应该说,WPF极大地简化了我们的打印输出工作,想过去使用VC++做开发的时候,打印及预览可是一件极麻烦的事情,而现在我不会再使用C++来做Windows的桌面应用了——性价比实在太低。

WPF的打印功能是很强大而简便的,它甚至能够直接打印界面上的内容,包括各种控件的显示内容,例如你在界面上摆放了一个datagrid控件,画了一个五角星,或写了一段文字,都可以直接打印出来,这里有一篇文章很简单明了地说明了这个功能:

http://www.cnblogs.com/gnielee/archive/2010/07/02/wpf-print-sample.html

这种做法是非常直截了当的,但恐怕不是很适合我们一般的应用,我们更多的时候需要自适应纸张,表格输出,自动分页,还有分页预览……

自己设计分页是非常麻烦的事情,没做过的人恐怕没法理解为什么,我这里插点题外话提一下,为什么分页难做?那是因为:你不真正把文档打印出来,你就不知道到底要在什么地方分页。举个最简单的例子,就一大段文本,给你打印,你认为到第几个字要另开一页?你估算了一下:一行20个字,我的打印纸一共20行,所以到第400个字的时候分页。——Too simple,你没考虑一行的字数根本就是不确定的(字符非等宽),也没考虑回车换行所产生的空行,更没考虑字体大小,行间距等影响因素,另外还有单词自适应因素,最后还有纸张大小……Oh,my god,这简直没法做,是的,自己很难做的,一个比较笨但有效的方法是“模拟打印”,用二分法找到开始分页的那个点,我以前做过的一个手机看书软件就是这么干的,而真实的分页算法是很复杂的,所幸的是这次不需要我们来做了。下面是我写的一个demo。

这是打印预览效果:

代码并不多。设计的思路就是:文档模版(xaml)+数据(.net对象)=打印输出

文档模版可以单独创建,右击你的WPF工程,Add – New Item – Flow Document(WPF),Visual Studio并没有提供这个xaml的预览,这点不得不说是个缺陷,微软的理由是这种Flow Document的显示需要一个容器,单独的Flow Document(流文档)是没法预览的,你必须把它放在一个容器中才可以,流文档的容器有FlowDocumentScrollViewer,FlowDocumentPageViewer,FlowDocumentReader,另外还有DocumentViewer,这个只支持固定流文档(只读)。关于流文档及其打印方面的技术在《WPF编程宝典》一书中都有具体讲述,建议大家要详细了解的话先去阅读一下此书,下面主要是一些书中没有的内容。

打印预览,我们这次选择了DocumentViewer,因为它直接就带有很好的分页功能,我们只需要生成固定文档(XPS),然后交给它,它就能很好的将内容预览出来——太棒了。

现在我们大致看看这个流文档模版的内容:

复制代码
    <Table FontSize="16">
        <Table.Columns>
            <TableColumn Width="200"></TableColumn>
            <TableColumn Width="600"></TableColumn>
        </Table.Columns>
        <TableRowGroup>
            <TableRow>
                <TableCell>
                    <Paragraph>
                        订单号
                    </Paragraph>
                </TableCell>
                <TableCell>
                    <Paragraph>
                        <Run Text="{Binding OrderNo}"></Run>
                    </Paragraph>
                </TableCell>
            </TableRow>
            <TableRow>
                <TableCell>
                    <Paragraph>
                        客户名称
                    </Paragraph>
                </TableCell>
                <TableCell>
                    <Paragraph>
                        <Run Text="{Binding CustomerName}"></Run>
                    </Paragraph>
                </TableCell>
            </TableRow>
            <!-- 省略一大段 -->
        </TableRowGroup>
    </Table>
复制代码

我把多余的内容去掉了,现在注意看“<Run Text=”{Binding OrderNo}”></Run>”这个地方,我将这个Run的Text属性绑定到DataContext的OrderNo去了,也就是说,它会根据数据的内容,渲染出不同的结果。

这里一切OK,但最大的问题来了:流文档的Table却不能跟UIElement的DataGrid控件那样能动态地根据数据的条目数渲染出相应的行!也就是说Table的行数是固定的,流文档上的对象是静态的,所以我们只能用后台代码来手工改变它了,这是相当不方便的地方……我定义了这么一个接口来做这种工作:

    public interface IDocumentRenderer
    {
        void Render(FlowDocument doc, Object data);
    }

创建一个对象,实现这个接口,然后根据data的内容,往doc里对应的地方插入行。

另外还需要特别说明的是代码中使用了一些BeginInvoke,也许大家不太了解那是什么意思,为什么需要这么麻烦?其实,那是因为你给Document的DataContext赋值的时候,Document的内容并不是马上改变的,不信你可以把我写的这些BeginInvoke改为直接调用,然后看看打印预览的文档内容,是不是哪些binding的地方还是空白的?所以需要一个“延后”调用。关于BeginInvoke的内容可以看我这篇blog:

http://www.cnblogs.com/guogangj/archive/2013/01/22/2870590.html

最后,主界面上的“直接打印”为了防止用户连续点击,需要在点了一下之后把它变灰,然后过几秒钟之后再把它变亮。

最后的最后:完整代码下载

 

#1楼 2013-09-17 15:57 下~站  
如果是多个表格要同时预览并且分页显示,这样怎么做啊,能不能给我说一下,谢谢!

#2楼 2013-09-29 17:05 tangbo1250  

你好!我想问一下DocumentViewer控件显示能不能是横向的啊?你这个可以实现纵向打印预览!不能把纸张横着!

#3楼[楼主2013-10-09 17:00 guogangj  

@ 下~站
在我的另一篇关于WPF打印预览的文章的留言中提到如何新开一页:http://www.cnblogs.com/guogangj/archive/2013/03/12/2955537.html

#4楼[楼主2013-10-10 08:55 guogangj  

@ tangbo1250
可以通过修改FlowDocument的PageWidth和PageHeight实现,在我提供的demo中,PrintPreviewWindow.xaml.cs里的“LoadDocumentAndRender”方法里,加上:

1
2
doc.PageWidth = 1122;
doc.PageHeight = 794;

1122是11.692英寸(A4纸高度)*96DPI得出,而794则是8.267英寸(A4纸宽度)*96DPI得出。当然,这样只是改变了所谓“打印预览”的外观,实际打印横竖向则需要在打印对话框中调整。(可以通过代码)

#5楼 2013-10-14 13:58 tangbo1250  

@ guogangj
谢谢,可以横做了!

#6楼 2013-12-17 12:00 kingcami  

不错,正好有这需求,谢谢楼主

#7楼 2014-04-12 19:42 kzar476  

感谢博主, 有代码非常棒! 支持

#8楼 2014-10-18 21:32 小小洪  

表格打印怎么让它每页都有header

#9楼[楼主2014-10-19 08:43 guogangj  

@ 小小洪
看我前面的留言。

#10楼 2015-11-26 11:26 UCI  

感谢楼主的分享!个人很喜欢楼主这种语言风格!

#11楼 2016-01-05 16:53 UCI  

有个问题 想请教一下 我想修改一下打印的内容,下面这个是我在原来基础改的,这些变量的值只会在程序运行开始的时候可以修改,运行中就不能更改了,我还是个菜鸟,期望您的解答!!

1
2
3
4
5
6
7
8
public static OderData m_orderExample = new OderData
       {
           OrderNo = PublicData.name,
           CustomerName = PublicData.sex,
           ShipAddress = PublicData.age,
           Express = "门诊",
           Freight = PublicData.ID,
       };

#12楼[楼主2016-01-06 22:21 guogangj  

@ UCI
为啥不能改变?GlobalData.m_orderExample.CustomerName=”老王”;这样不行么?
我当初代码这样写,把这些“数据”声明为static完全是为了方便演示打印功能,实际应用中基本上不会这样写的,另外回看自己几年前的C#代码,由于当时刚开始弄.net,居然还带着浓浓的C++(MFC)风格,今非昔比,今非昔比……

#13楼 2016-01-07 09:21 UCI  

@ guogangj
十分感谢,没想到还能收到您的回复,问题终于解决了,由于目前还在学习WPF中,对于基础掌握还不是很好,看您的代码没有能全部看明白,您的这个回复简就是一语中的,再次感谢一下!!!

#14楼 2016-02-25 13:51 ll554749099  

楼主,如何打印用户控件呢?

#15楼[楼主2016-02-25 22:44 guogangj  

@ ll554749099
这篇文章开头提供的那个链接,看看

#16楼 2016-03-10 16:05 比尔盖熊  

有个问题不知道博主有没有解决:从预览窗口中,直接点击打印,传送到打印队列的文档大小竟然可以达到几十兆,甚至更大。不知道这是什么原因引起。这个问题如果是在网络打印的情况下,那是相当麻烦的,会传送很久都没能传到打印机那里。但是保存成XPS文档后再打印,倒是没有问题,只是无疑操作步骤多了一步了。

#17楼[楼主2016-03-10 21:12 guogangj  

@ 比尔盖熊
之前没发现你说的问题,而现在我暂时没有真实打印机尝试,Visual Studio也暂时没有(囧),有条件时候我再试试看。

#18楼 2016-03-30 16:23 孟书  

@ 比尔盖熊
因为xps是压缩的 你把xps解压缩了再发送打印 大小应该是差不多的,以前似乎遇到过

#19楼 2016-03-30 16:25 孟书  

@ 比尔盖熊
话说 我刚才试了一下楼主的代码 好像没有你出的问题啊 打印列表一闪而过了没来得及截图 但是肯定没有几十兆那么大

#20楼 2016-04-08 15:25 比尔盖熊  

@ 孟书
不用接到真实打印机,就在你电脑上随便找个驱动装上去,打印的时候就选择这个刚装上去的打印机。
然后,控制面板->设备和打印机->选择刚装的打印机右键->”查看现在正在打印什么”.
看到的这个打印任务列表,就可以看到传送文件的大小了。
像博文中的示例,打印文件应该几十K,而经观察就有10多M。

#21楼 2016-04-08 15:28 比尔盖熊  

这个可能是微软打印的一个坑,给了第三方专门搞打印的一个市场空间了。

#22楼 2016-09-28 17:10 SmileZhen  

请问楼主,这个可以运用到快递单打印吗?我使用的是调用浏览器的打印函数套打,但是打印位置很不好调,没办法做到很准确的打印在应打印的位置,调地真是心力憔悴啊。

WPF打印原理,自定义打印

一.基础知识

1.System.Printing命名空间

我们可以先看一下System.Printing命名空间,东西其实很多,功能也非常强大,可以说能够控制打印的每一个细节,曾经对PrintDialog失望的我看到了一丝曙光。

2.PrintDialog

可以看到PrintDialog除了构造函数有三个方法和一堆属性,PrintDocument接受一个分页器(DocumentPaginator,稍后介绍),PrintVisual可以打印Visual,也就是WPF中的大部分继承自Visual类的UI对象都可以打印出来,最后一个是ShowDialog方法,其实就是显示一个界面,可以配置一下纸张选择,横向打印还是纵向打印,但是其打印范围页的功能是没有实现的,无论怎么配置,都是全部打印出来,这个稍后会有解决办法。

至此,可以看出如果我们要随心所欲打印自己的东西那么PrintDialog一个是不够用的,要能够打印自定义的内容我们需要使用到强大的DocumentPaginator。

3.DocumentPaginator

DocumentPaginator是一个抽象类,我们继承其看需要重写哪些东西

class TestDocumentPaginator : DocumentPaginator
    {
        public override DocumentPage GetPage(int pageNumber)
        {
            throw new NotImplementedException();
        }

        public override bool IsPageCountValid
        {
            get
            {
                return true;
            }
        }

        public override int PageCount
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override Size PageSize
        {
            get;
            set;
        }

        public override IDocumentPaginatorSource Source
        {
            get
            {
                return null;
            }
        }
    }

注意GetPage方法,这个很重要,这也是分页器的核心所在,我们根据传入的页码返回内容DocumentPage,IsPageCountValid直接设置为True即可,PageCount即总页数,这个需要我们根据需求来分页来计算,PageSize就是纸张的大小,至于Source是用在什么地方还真没研究过,直接返回null。如何实现自定义打印稍后介绍。

3.PrintServer && PrintQueue

PrintServer可以获取本地的打印机列表或网络打印机,PrintQueue实际上代表的就是一个打印机,所以我们就能够获取到本地计算机上已经配置的打印机,还能够获取默认打印机哦

private void LoadPrinterList()
        {
           var printServer = new PrintServer();

            //获取全部打印机
            PrinterList = printServer.GetPrintQueues();

            //获取默认打印机
            DefaultPrintQueue = LocalPrintServer.GetDefaultPrintQueue();
        }

 

4.PageMeidaSize && PageMediaSizeName

PageMediaSize包含了纸张的宽和高以及名称,PageMediaSizeName是一个枚举,把所有纸张的名称都列举出来了,所以我们就能够获取到打印机支持的纸张类型集合了

var pageSizeCollection = DefaultPrintQueue.GetPrintCapabilities().PageMediaSizeCapability;

 

二.自定义打印原理

我们看一下DocumentPage这个对象,构造函数需要传入一个Visual对象,打印的每一页其实就是打印每一页的Visual,这就好办了,WPF中有一个Visual的派生类DrawingVisual,DrawingVisual好比一个“画板”,我们可以在上面任意作画,有了画板我们还要拥有“画笔”DrawingContext。马上演示如何在画板上作画

private void DrawSomething()
        {
            var visual = new DrawingVisual();

            using (DrawingContext dc = visual.RenderOpen())
            {
                dc.DrawRectangle(Brushes.Black, new Pen(Brushes.Black, 1), new Rect(0, 0, 100, 100));
            }
        }

这样我就在左上角绘制了一个宽100高100的矩形,DrawingContext的方法很多

可以看到能够绘制许多基本的东西,如图片,文本,线段等。

到这儿,大家都该清楚了,自定义打印的原理就是使用DrawingVisual绘制自己的内容,然后交给DocumentPage,让打印机来处理。

下面演示一下打印5个页面,每个页面左上角显示页码

TestDocumentPaginator.cs

class TestDocumentPaginator : DocumentPaginator
    {

        #region 字段
         private int _pageCount;
        private Size _pageSize;
        #endregion

        #region 构造
        public TestDocumentPaginator()
        {
            //这个数据可以根据你要打印的内容来计算
            _pageCount = 5;

            //我们使用A3纸张大小
            var pageMediaSize = LocalPrintServer.GetDefaultPrintQueue()

 

.GetPrintCapabilities()

 

.PageMediaSizeCapability

 

.FirstOrDefault(x => x.PageMediaSizeName == PageMediaSizeName.ISOA3);

            if (pageMediaSize != null)
            {
                _pageSize = new Size((double)pageMediaSize.Width, (double)pageMediaSize.Height);
            }
        }
        #endregion

        #region 重写
        /// <summary>
        /// 
        /// </summary>
        /// <param name="pageNumber">打印页是从0开始的</param>
        /// <returns></returns>
        public override DocumentPage GetPage(int pageNumber)
        {
            var visual = new DrawingVisual();

            using (DrawingContext dc = visual.RenderOpen())
            {
                //设置要绘制的文本,文本字体,大小,颜色等
                FormattedText text = new FormattedText(string.Format("第{0}页", pageNumber + 1),

 

CultureInfo.CurrentCulture,

 

FlowDirection.LeftToRight,

 

new Typeface("宋体"),

 

30,

 

Brushes.Black);

                //文本的左上角位置
                Point leftpoint = new Point(0, 0);

                dc.DrawText(text, leftpoint);
            }

            return new DocumentPage(visual, _pageSize, new Rect(_pageSize), new Rect(_pageSize));
        }

        public override bool IsPageCountValid
        {
            get
            {
                return true;
            }
        }

        public override int PageCount
        {
            get
            {
                return _pageCount;
            }
        }

        public override Size PageSize
        {
            get
            {
                return _pageSize;
            }
            set
            {
                _pageSize = value;
            }
        }

        public override IDocumentPaginatorSource Source
        {
            get
            {
                return null;
            }
        }
        #endregion
    }

 

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            PrintDialog p = new PrintDialog();

            TestDocumentPaginator docPaginator = new TestDocumentPaginator();

            p.PrintDocument(docPaginator, "测试");
        }

注意,这里我使用了MicroSoft的虚拟打印机XPS,然后使用XPS查看器查看

这样一共5页

三.打印范围页

我在使用PrintDialog的时候,尝试过打印范围页,就通过设置PrintDialog的几个参数,但都失败了,网上一搜,遇到此问题的少年还不少,于是网上有许多办法,比较容易搜到的是一个从PrintDialog派生类然后自己处理打印范围页,这个方法想法是好的,但是内部理解起来不容易,一定有更合适的方法,于是各种搜索(Google不能用了,只好用Bing),搜到这么一篇文章How to print a PageRange with WPF’s PrintDialog,文章没有讲得很清晰,其实原理很简单

对,分页器的分页器…,我们使用第二个分页器,在页码上加上一个基数,取第一个分页器的页面,也不知大家看明白没有,算了,我还是上代码吧

PageRangeDocumentPaginator.cs

class PageRangeDocumentPaginator : DocumentPaginator
    {
        private int _startIndex;
        private int _endIndex;
        private DocumentPaginator _paginator;

        public PageRangeDocumentPaginator(int startIndex, int endIndex, DocumentPaginator paginator)
        {
            _startIndex = startIndex;
            _endIndex = endIndex;
            _paginator = paginator;
        }

        public override DocumentPage GetPage(int pageNumber)
        {
            return _paginator.GetPage(pageNumber + _startIndex);
        }

        public override bool IsPageCountValid
        {
            get
            {
                return _paginator.IsPageCountValid;
            }
        }

        public override int PageCount
        {
            get
            {
                return _endIndex - _startIndex + 1;
            }
        }

        public override Size PageSize
        {
            get
            {
                return _paginator.PageSize;
            }
            set
            {
                _paginator.PageSize = value;
            }
        }

        public override IDocumentPaginatorSource Source
        {
            get
            {
                return null;
            }
        }
    }

这个方法实现很简单,也很巧妙。

四.打印预览

我们有了分页器,并且能够从分页器中GetPage(int pageNumber),得到某一页的DocumentPage,DocumentPage中包含了我们绘制的Visual,这个时候就可以将Visual拿出来,用一个Canvas在窗口上显示出来,达到一个预览的效果,但Canvas需要特殊处理一下

DrawingCanvas.cs

class DrawingCanvas : Canvas
    {
        #region 字段
        private List<Visual> _visuals = new List<Visual>();
        #endregion

        #region 公有方法

        public void AddVisual(Visual visual)
        {
            _visuals.Add(visual);

            base.AddLogicalChild(visual);
            base.AddVisualChild(visual);
        }

        public void RemoveVisual(Visual visual)
        {
            _visuals.Remove(visual);

            base.RemoveLogicalChild(visual);
            base.RemoveVisualChild(visual);
        }

        public void RemoveAll()
        {
            while (_visuals.Count != 0)
            {
                base.RemoveLogicalChild(_visuals[0]);
                base.RemoveVisualChild(_visuals[0]);

                _visuals.RemoveAt(0);
            }
        }

        #endregion

        #region 构造

        public DrawingCanvas()
        {
            Width = 200;
            Height = 200;
        }
        #endregion

        #region 重写
        protected override int VisualChildrenCount
        {
            get
            {
                return _visuals.Count;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            return _visuals[index];
        }
        #endregion
    }

 

这样就可以直接用Canvas直接Add我们的Visual了

五.异步打印

为什么会想到使用异步打印呢?当要打印的页面数量非常大的时候,比如400多页,在使用PrintDialog.PrintDocument的时候,会卡住界面很久,这不是我们所希望的。

其实PrintDialog内部是使用了XpsDocumentWriter的,它有一个WriteAsync方法

var doc = PrintQueue.CreateXpsDocumentWriter(queue);

doc.WriteAsync(new PageRangeDocumentPaginator(startIndex, endIndex, p));

但是啊,这么做还是不能完全解决界面卡住的问题,为什么呢?因为我们的分页器使用了DrawingVisual,Visual是DispatcherObject的派生类,那么对它的使用是要占用UI线程资源的。而我们的分页器是在主UI线程中创建的,异步方法其实是另开一个线程去处理,那么这个线程对Visual的访问还是会切换到主线程上,要不就会报错…,好吧,干脆开一个线程,重新创建分页器,创建XpsDocumentWriter,整个一套都在一个单独的线程中执行,于是

Task.Factory.StartNew(() =>
                    {
                        try
                        {
                            var p = PaginatorFactory.GetDocumentPaginator(_config);

                            p.PageSize = new Size(_paginator.PageSize.Width, _paginator.PageSize.Height);

                            var server = new LocalPrintServer();

                            var queue = server.GetPrintQueue(queueName);

                            queue.UserPrintTicket.PageMediaSize = PageSize;

                            queue.UserPrintTicket.PageOrientation = PageOrientation;

                            var doc = PrintQueue.CreateXpsDocumentWriter(queue);

                          }
                        catch (Exception ex)
                        {

                        }
                        finally
                        {
                            _dispatcher.BeginInvoke(new Action(() => Close()));
                        }
                    });

一试,界面完全不卡,因为这个时候已经不关UI线程的事了,需要注意一点就是,已经单独在一个线程中,那么就不需要使用异步打印方法了即WriteAsync,使用Writer即可,大家试一下就知道了。

六.源码

项目是一个实现打印预览的功能,目前我已经实现了DataTable的打印预览,BitmapImage和FrameworkElement的打印预览,后两者暂不支持完全异步的打印。

源码托管在开源中国:https://git.oschina.net/HelloMyWorld/HappyPrint.git,第一次把自己的东西共享出来,希望大家支持和斧正。

参考资料:《WPF编程宝典》第29章打印

WPF打印原理,自定义打印

一.基础知识

1.System.Printing命名空间

我们可以先看一下System.Printing命名空间,东西其实很多,功能也非常强大,可以说能够控制打印的每一个细节,曾经对PrintDialog失望的我看到了一丝曙光。

 

2.PrintDialog

可以看到PrintDialog除了构造函数有三个方法和一堆属性,PrintDocument接受一个分页器(DocumentPaginator,稍后介绍),PrintVisual可以打印Visual,也就是WPF中的大部分继承自Visual类的UI对象都可以打印出来,最后一个是ShowDialog方法,其实就是显示一个界面,可以配置一下纸张选择,横向打印还是纵向打印,但是其打印范围页的功能是没有实现的,无论怎么配置,都是全部打印出来,这个稍后会有解决办法。

至此,可以看出如果我们要随心所欲打印自己的东西那么PrintDialog一个是不够用的,要能够打印自定义的内容我们需要使用到强大的DocumentPaginator。

 

3.DocumentPaginator

DocumentPaginator是一个抽象类,我们继承其看需要重写哪些东西

复制代码
class TestDocumentPaginator : DocumentPaginator
    {
        public override DocumentPage GetPage(int pageNumber)
        {
            throw new NotImplementedException();
        }

        public override bool IsPageCountValid
        {
            get
            {
                return true;
            }
        }

        public override int PageCount
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override Size PageSize
        {
            get;
            set;
        }

        public override IDocumentPaginatorSource Source
        {
            get
            {
                return null;
            }
        }
    }
复制代码

注意GetPage方法,这个很重要,这也是分页器的核心所在,我们根据传入的页码返回内容DocumentPage,IsPageCountValid直接设置为True即可,PageCount即总页数,这个需要我们根据需求来分页来计算,PageSize就是纸张的大小,至于Source是用在什么地方还真没研究过,直接返回null。如何实现自定义打印稍后介绍。

 

3.PrintServer && PrintQueue

PrintServer可以获取本地的打印机列表或网络打印机,PrintQueue实际上代表的就是一个打印机,所以我们就能够获取到本地计算机上已经配置的打印机,还能够获取默认打印机哦

复制代码
        private void LoadPrinterList()
        {
           var printServer = new PrintServer();

            //获取全部打印机
            PrinterList = printServer.GetPrintQueues();

            //获取默认打印机
            DefaultPrintQueue = LocalPrintServer.GetDefaultPrintQueue();
        }
复制代码

4.PageMeidaSize && PageMediaSizeName

PageMediaSize包含了纸张的宽和高以及名称,PageMediaSizeName是一个枚举,把所有纸张的名称都列举出来了,所以我们就能够获取到打印机支持的纸张类型集合了

var pageSizeCollection = DefaultPrintQueue.GetPrintCapabilities().PageMediaSizeCapability;

 

 

二.自定义打印原理

我们看一下DocumentPage这个对象,构造函数需要传入一个Visual对象,打印的每一页其实就是打印每一页的Visual,这就好办了,WPF中有一个Visual的派生类DrawingVisual,DrawingVisual好比一个“画板”,我们可以在上面任意作画,有了画板我们还要拥有“画笔”DrawingContext。马上演示如何在画板上作画

复制代码
private void DrawSomething()
        {
            var visual = new DrawingVisual();

            using (DrawingContext dc = visual.RenderOpen())
            {
                dc.DrawRectangle(Brushes.Black, new Pen(Brushes.Black, 1), new Rect(0, 0, 100, 100));
            }
        }
复制代码

这样我就在左上角绘制了一个宽100高100的矩形,DrawingContext的方法很多

可以看到能够绘制许多基本的东西,如图片,文本,线段等。

到这儿,大家都该清楚了,自定义打印的原理就是使用DrawingVisual绘制自己的内容,然后交给DocumentPage,让打印机来处理。

下面演示一下打印5个页面,每个页面左上角显示页码

TestDocumentPaginator.cs

复制代码
class TestDocumentPaginator : DocumentPaginator
    {

        #region 字段
         private int _pageCount;
        private Size _pageSize;
        #endregion

        #region 构造
        public TestDocumentPaginator()
        {
            //这个数据可以根据你要打印的内容来计算
            _pageCount = 5;

            //我们使用A3纸张大小
            var pageMediaSize = LocalPrintServer.GetDefaultPrintQueue()
                              .GetPrintCapabilities()
                              .PageMediaSizeCapability
                              .FirstOrDefault(x => x.PageMediaSizeName == PageMediaSizeName.ISOA3);

            if (pageMediaSize != null)
            {
                _pageSize = new Size((double)pageMediaSize.Width, (double)pageMediaSize.Height);
            }
        }
        #endregion

        #region 重写
        /// <summary>
        /// 
        /// </summary>
        /// <param name="pageNumber">打印页是从0开始的</param>
        /// <returns></returns>
        public override DocumentPage GetPage(int pageNumber)
        {
            var visual = new DrawingVisual();

            using (DrawingContext dc = visual.RenderOpen())
            {
                //设置要绘制的文本,文本字体,大小,颜色等
                FormattedText text = new FormattedText(string.Format("第{0}页", pageNumber + 1),
                                                     CultureInfo.CurrentCulture,
                                                     FlowDirection.LeftToRight,
                                                     new Typeface("宋体"),
                                                     30,
                                                     Brushes.Black);

                //文本的左上角位置
                Point leftpoint = new Point(0, 0);

                dc.DrawText(text, leftpoint);
            }

            return new DocumentPage(visual, _pageSize, new Rect(_pageSize), new Rect(_pageSize));
        }

        public override bool IsPageCountValid
        {
            get
            {
                return true;
            }
        }

        public override int PageCount
        {
            get
            {
                return _pageCount;
            }
        }

        public override Size PageSize
        {
            get
            {
                return _pageSize;
            }
            set
            {
                _pageSize = value;
            }
        }

        public override IDocumentPaginatorSource Source
        {
            get
            {
                return null;
            }
        }
        #endregion
    }
复制代码

复制代码
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            PrintDialog p = new PrintDialog();

            TestDocumentPaginator docPaginator = new TestDocumentPaginator();

            p.PrintDocument(docPaginator, "测试");
        }
复制代码

 

注意,这里我使用了MicroSoft的虚拟打印机XPS,然后使用XPS查看器查看

 

这样一共5页

 

 

三.打印范围页

我在使用PrintDialog的时候,尝试过打印范围页,就通过设置PrintDialog的几个参数,但都失败了,网上一搜,遇到此问题的少年还不少,于是网上有许多办法,比较容易搜到的是一个从PrintDialog派生类然后自己处理打印范围页,这个方法想法是好的,但是内部理解起来不容易,一定有更合适的方法,于是各种搜索(Google不能用了,只好用Bing),搜到这么一篇文章How to print a PageRange with WPF’s PrintDialog,文章没有讲得很清晰,其实原理很简单

对,分页器的分页器…,我们使用第二个分页器,在页码上加上一个基数,取第一个分页器的页面,也不知大家看明白没有,算了,我还是上代码吧

PageRangeDocumentPaginator.cs

复制代码
class PageRangeDocumentPaginator : DocumentPaginator
    {
        private int _startIndex;
        private int _endIndex;
        private DocumentPaginator _paginator;

        public PageRangeDocumentPaginator(int startIndex, int endIndex, DocumentPaginator paginator)
        {
            _startIndex = startIndex;
            _endIndex = endIndex;
            _paginator = paginator;
        }

        public override DocumentPage GetPage(int pageNumber)
        {
            return _paginator.GetPage(pageNumber + _startIndex);
        }

        public override bool IsPageCountValid
        {
            get
            {
                return _paginator.IsPageCountValid;
            }
        }

        public override int PageCount
        {
            get
            {
                return _endIndex - _startIndex + 1;
            }
        }

        public override Size PageSize
        {
            get
            {
                return _paginator.PageSize;
            }
            set
            {
                _paginator.PageSize = value;
            }
        }

        public override IDocumentPaginatorSource Source
        {
            get
            {
                return null;
            }
        }
    }
复制代码

这个方法实现很简单,也很巧妙。

 

 

四.打印预览

我们有了分页器,并且能够从分页器中GetPage(int pageNumber),得到某一页的DocumentPage,DocumentPage中包含了我们绘制的Visual,这个时候就可以将Visual拿出来,用一个Canvas在窗口上显示出来,达到一个预览的效果,但Canvas需要特殊处理一下

DrawingCanvas.cs

复制代码
class DrawingCanvas : Canvas
    {
        #region 字段
        private List<Visual> _visuals = new List<Visual>();
        #endregion

        #region 公有方法

        public void AddVisual(Visual visual)
        {
            _visuals.Add(visual);

            base.AddLogicalChild(visual);
            base.AddVisualChild(visual);
        }

        public void RemoveVisual(Visual visual)
        {
            _visuals.Remove(visual);

            base.RemoveLogicalChild(visual);
            base.RemoveVisualChild(visual);
        }

        public void RemoveAll()
        {
            while (_visuals.Count != 0)
            {
                base.RemoveLogicalChild(_visuals[0]);
                base.RemoveVisualChild(_visuals[0]);

                _visuals.RemoveAt(0);
            }
        }

        #endregion

        #region 构造

        public DrawingCanvas()
        {
            Width = 200;
            Height = 200;
        }
        #endregion

        #region 重写
        protected override int VisualChildrenCount
        {
            get
            {
                return _visuals.Count;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            return _visuals[index];
        }
        #endregion
    }
复制代码

这样就可以直接用Canvas直接Add我们的Visual了

五.异步打印

为什么会想到使用异步打印呢?当要打印的页面数量非常大的时候,比如400多页,在使用PrintDialog.PrintDocument的时候,会卡住界面很久,这不是我们所希望的。

其实PrintDialog内部是使用了XpsDocumentWriter的,它有一个WriteAsync方法

var doc = PrintQueue.CreateXpsDocumentWriter(queue);

doc.WriteAsync(new PageRangeDocumentPaginator(startIndex, endIndex, p));

但是啊,这么做还是不能完全解决界面卡住的问题,为什么呢?因为我们的分页器使用了DrawingVisual,Visual是DispatcherObject的派生类,那么对它的使用是要占用UI线程资源的。而我们的分页器是在主UI线程中创建的,异步方法其实是另开一个线程去处理,那么这个线程对Visual的访问还是会切换到主线程上,要不就会报错…,好吧,干脆开一个线程,重新创建分页器,创建XpsDocumentWriter,整个一套都在一个单独的线程中执行,于是

复制代码
Task.Factory.StartNew(() =>
                    {
                        try
                        {
                            var p = PaginatorFactory.GetDocumentPaginator(_config);

                            p.PageSize = new Size(_paginator.PageSize.Width, _paginator.PageSize.Height);

                            var server = new LocalPrintServer();

                            var queue = server.GetPrintQueue(queueName);

                            queue.UserPrintTicket.PageMediaSize = PageSize;

                            queue.UserPrintTicket.PageOrientation = PageOrientation;

                            var doc = PrintQueue.CreateXpsDocumentWriter(queue);

                          }
                        catch (Exception ex)
                        {

                        }
                        finally
                        {
                            _dispatcher.BeginInvoke(new Action(() => Close()));
                        }
                    });
复制代码

一试,界面完全不卡,因为这个时候已经不关UI线程的事了,需要注意一点就是,已经单独在一个线程中,那么就不需要使用异步打印方法了即WriteAsync,使用Writer即可,大家试一下就知道了。

六.源码