前言
开发WinForm应用,打印预览是经常要用到的功能,使用方法小伙伴们都知道,就像下面这样:
printPreviewDialog1.Document = printDocument1;
printPreviewDialog1.ShowDialog();
先拖一个PrintPreviewDialog控件,传入一个PrintDocument属性然后展示。
效果如下。
这样功能是能用,不过这个预览窗口有几个小问题:
- 窗口图标是默认的Windows图标,不符合应用整体风格
- 标题和按钮及悬停提示是英文的。用户文化水平参差不齐,有的可能连Close这个单词也不认识啊。
- 工具栏最左边的那个打印按钮实在太小,用户注意不到。
- 打印预览窗口出现的位置很随机,每次飘忽不定,我们希望出现在正中间
这些小问题虽然无伤大雅,但对于一个一贯用高标准严要求对待自己的完美主义程序员来说是不满意的。
我们希望把它优化一下。
优化步骤
标题与图标
如果我们打开默认的PrintPreviewDialog控件只能看到这么几个属性,其中一个最重要的是PrintDocument,这个是传打印内容的。
如果我们想改下标题和图标怎么办呢?可上面的属性窗口并没有啊。没办法,这时候
我们点开PrintPreviewDialog的源代码瞄一下,可以看到
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new Icon Icon
{
get
{
return base.Icon;
}
set
{
base.Icon = value;
}
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override string Text
{
get
{
return base.Text;
}
set
{
base.Text = value;
}
}
原来这个预览窗的作者很不地道,故意把这2个属性给隐藏了。
可能这个作者没想到Print Preview这个标题还有人想改吧。他忘记14亿中国人也要用啊。
通过分析代码,我们可以知道,虽然属性窗口看不到,不过我们可以通过代码赋值。
printPreviewDialog1.Icon = Properties.Resources.my_icon;
printPreviewDialog1.Text = "打印预览";
窗口位置
同样的,我们可以通过代码把窗口定位到中央,免得每次打开时预览窗口位置飘忽不定。
((Form)printPreviewDialog1).StartPosition = FormStartPosition.CenterScreen;
注意这个StartPosition属性必须强制转换到父类Form后才能使用,
按钮英文汉化
好了,现在我们再看看怎么汉化按钮上的一些英文,首先从UI上看,这是一排工具栏,上面的按钮一个是一个个的对像,我们应该可以找到这些按钮替换掉Text和ToolTipText. 再次打开PrintPreviewDialog的源代码,踅摸一下:
八九不离十,这个应该就是工具栏上个的打印按钮了。
但是这个是private 字段,我们是不能直接修改Text 和ToolTipText的,怎么办呢?难到就没辙了吗?
这时候,我们该祭出虚拟机语言的大杀器——反射了。
我们可以通过反射得到这个私有变量,然后再赋值。
方法如下:
FieldInfo fi = typeof(PrintPreviewDialog).GetField("printToolStripButton", BindingFlags.NonPublic | BindingFlags.Instance);
object o = fi.GetValue(this);
var t1 = (ToolStripButton)o;
t1.Text = "打印";
t1.ToolTipText = "打印";
t1.DisplayStyle = ToolStripItemDisplayStyle.ImageAndText; //打印按钮也显示文字这样看得更清楚
注意反射获取字段域的用法。前面用typeof得到PrintPreviewDialog的类型,
GetField方法里面的第一个参数是变量名,字符串,第2个变量属性非常重要,
BindingFlags.NonPublic表示取的是非public字段, BindingFlags.Instance是实体字段(非静态)
这个参数没指明清楚是获取不到字段域的,会得到null
得到反射后的字段对象后,通过GetValue我们就可以得到这个私有字段的实体了,
object o = fi.GetValue(printPreviewDialog1);
然后就可以给它赋值了。
t1.Text = text;
t1.ToolTipText = text;
……
同样的方法我们可以把其它的几个按钮都汉化。
注意其中缩放按钮的类型是ToolStripSplitButton,右边的“页”的类型是ToolStripLabel
因为按钮比较多,为了避免写重复代码,我们把这个动作封装成一个通用的方法:
private void LocalizeOne(string name, string text)
{
FieldInfo fi = typeof(PrintPreviewDialog).GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
object o = fi.GetValue(this);
if (o is ToolStripButton t1)
{
t1.Text = text;
t1.ToolTipText = text;
if (name == "printToolStripButton")
{//如果是打印按钮,显示文字这样看得更清楚
t1.DisplayStyle = ToolStripItemDisplayStyle.ImageAndText;
}
}
else if (o is ToolStripSplitButton t2)
{
t2.ToolTipText = text;
}
else if (o is ToolStripLabel t3)
{
t3.Text = text;
}
}
然后可以这样调用(先在PrintPreviewDialog的源代码里瞄好各个按钮的变量名):
LocalizeOne("printToolStripButton", "打印");
LocalizeOne("zoomToolStripSplitButton", "缩放");
LocalizeOne("onepageToolStripButton", "一页");
LocalizeOne("twopagesToolStripButton", "两页");
LocalizeOne("threepagesToolStripButton", "三页");
LocalizeOne("fourpagesToolStripButton", "四页");
LocalizeOne("sixpagesToolStripButton", "六页");
LocalizeOne("closeToolStripButton", "关闭");
LocalizeOne("pageToolStripLabel", "页");
包装成一个用户控件
有了这些准备,实际上我们把这些全部封装到一个用户控件里去,不用每次写这些冗余代码。
这个控件的完整内容如下:
/// <summary>
/// 汉化后的预览窗口
/// </summary>
public partial class MyPrintPreviewDialog : PrintPreviewDialog
{
[Browsable(true)]
public new Icon Icon
{
get { return ((Form)this).Icon; }
set { ((Form)this).Icon = value; }
}
[Browsable(true)]
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public MyPrintPreviewDialog()
{
InitializeComponent();
}
private void MyPrintPreviewDialog_Load(object sender, System.EventArgs e)
{
Text = "打印预览";
LocalizeOne("printToolStripButton", "打印");
LocalizeOne("zoomToolStripSplitButton", "缩放");
LocalizeOne("onepageToolStripButton", "一页");
LocalizeOne("twopagesToolStripButton", "两页");
LocalizeOne("threepagesToolStripButton", "三页");
LocalizeOne("fourpagesToolStripButton", "四页");
LocalizeOne("sixpagesToolStripButton", "六页");
LocalizeOne("closeToolStripButton", "关闭");
LocalizeOne("pageToolStripLabel", "页");
}
private void LocalizeOne(string name, string text)
{
FieldInfo fi = typeof(PrintPreviewDialog).GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
object o = fi.GetValue(this);
if (o is ToolStripButton t1)
{
t1.Text = text;
t1.ToolTipText = text;
if (name == "printToolStripButton")
{//如果是打印按钮,显示文字这样看得更清楚
t1.DisplayStyle = ToolStripItemDisplayStyle.ImageAndText;
}
}
else if (o is ToolStripSplitButton t2)
{
t2.ToolTipText = text;
}
else if (o is ToolStripLabel t3)
{
t3.Text = text;
}
}
}
其中InitializeCompoment()是一段通用例程,也列在这里吧。
partial class MyPrintPreviewDialog
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region 组件设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// MyPrintPreviewDialog
//
this.ClientSize = new System.Drawing.Size(783, 556);
this.Name = "MyPrintPreviewDialog";
this.Load += new System.EventHandler(this.MyPrintPreviewDialog_Load);
this.ResumeLayout(false);
}
#endregion
}
然后你在你的自己需要的地方可以这么调用,拉这个用户控件到你自己的UI,假定名称为 printPreviewDialog1:
((Form)printPreviewDialog1).StartPosition = FormStartPosition.CenterScreen;
printPreviewDialog1.Icon = Properties.Resources.my_icon;
printPreviewDialog1.Width = 500;
printPreviewDialog1.Height = 800;
这样就大功告成了。完成后的效果是这样的。
是不是看起来顺眼很多呢。
预览用户控件代码可以直接拷贝拿去用。
总结
这篇文章所描述的是优化WinForm预览窗口的小窍门。
虽然功能不复杂,但用到了我们开发过程中的一些设计概念。如封装,用户控件,反射等。实际上并不是要做一个大型框架才叫设计,一个小的功能模块一样存在设计。有了设计的概念,我们在开发时可以把每一段代码优化到极致。
很多初学Java或C#的小伙伴总觉得反射这些功能比较神秘遥远,好像我一般也用不到,这个例子就用到了,带着问题去学习是快速提高自己能力的最好实践。