所见即所得的设计器

本文档介绍了实现一个所见即所得设计器的过程,包括文档的加载和保存、图形绘制、用户交互以及扩展性设计。设计器支持XML格式,以确保兼容性和可读性。通过优化绘制操作,减少屏幕闪烁,提高用户界面响应。同时,文章讨论了不同元素类型如矩形、线段、文本、表格和图片的实现,并探讨了双缓冲技术的利弊。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
本文讨论了如何使用C#编写一个所见即所得的设计器[GDesigner],分析了设计器的基本原理,可能遇到的技术问题,以及如何调用.NET框架来实现一个设计器。
    随着计算机信息系统不断深入发展,其系统结构要求越来越灵活,这种灵活性就是表现为程序的高度可配置性,可能应用程序的工作流程可以随便改变,用户界面也可以随便改变,面对这种不断增强的灵活,是不可能通过修改程序代码来实现的,应用系统本身需要发生深刻变化,需要实现很强的扩展性和灵活性。此外专门用于修改系统配置的外围工具也是非常重要的。这些系统外围定制工具很大一部分就是一些所见即所得的设计器。比如工作流编制工具,WorkFlowDesigner或FormDesigner界面设计器,而报表设计器也是典型的外围定制工具。
总所周知,所见即所得的设计器是个相当复杂的程序,首先它需要复杂的图形化用户界面编程,包括图形的绘制,鼠标键盘事件的处理,还要抗屏幕闪烁。其次还有它后台的数据维护处理,包括用户界面和数据的同步,数据的组织安排,以及加载和保存文档的处理。而且这些处理过程可以算是纠缠在一起,需要非常认真小心的分析设计,仔细编码。
      设计器按照用户界面和使用体验,可以分为两种模式,一种是基于直角坐标方式,另一种是基于流式排版方式。微软的Visio就是典型的直角坐标方式,而Word则是流式排版方式,而VS.NET的WebForm窗体设计器就是这两者的结合。
    在直角坐标方式的设计器中,设计元素是使用XY坐标来在设计视图中定位的,对于矩形元素一般指定它的左上角的位置来定位,设计者需要指定设计元素的位置,有时还要设置它的大小。对于线段需要指定两个端点的XY坐标。设计者只要设置好了各个元素的位置大小就完成了设计文档的基本结构,剩下的就是设置各个元素各自的内容了。
    在流式排版设计器中,设计元素是不需要指定位置的,是根据一般根据从左到右,从上到下的排列原则填充到设计视图中(但有时会变成其他排列原则)。设计元素的位置是动态计算的。流式设计器可能还要使用键盘直接输入文本,需要显示光标。流式排版设计器可以看作文字处理器。
    这两种设计器用户界面和使用体验不一样,因此其程序处理的方式也不一样,直角坐标设计器存在设计元素间相互覆盖,这影响绘图,此外还需要大量的鼠标拖拽操作,需要认真处理鼠标事件,但键盘事件处理得不多。而流式排版设计器中元素不会相互覆盖,因此绘制起来方便点,鼠标事件处理不多,但键盘事件处理的多,此外还需要处理光标。但这两种设计器它的文档对象模型有比较大的类似性。
    在本文中,以下只讨论直角坐标方式的设计器。
一个设计器应当实现的功能有
  1. 设计文档的加载和保存,设计器可以将当前设计的内容保存到一个文档中,这个文档可以保存到文件中,也可保存到数据库或某个服务器中。设计器可以加载文档来完全重现上次的设计结果。
  2. 设计器可以快速准确的绘制文档视图,当视图大小超过设计区域时,用户界面应当出现滚动条来进行滚动操作。
  3. 当前有互换式设计体验,用户可以使用鼠标拖拽操作来改变元素的位置大小等布局设置,用户改变了元素的布局或某些属性时,必须立即更新文档视图,而且更新区域应当尽量小。
  4. 支持所见即所得的设计体验,当设计器需要进行图形输出,例如输出图片或打印时,用户在设计器中的设计视图应当和输出的图形保持一致。
  5. 尽量减少屏幕闪烁。这需要绘制图形或更新视图时需要进行优化,尽快完成绘制操作。
  6. 若设计器需要进行扩展时,设计器应当提供足够的扩展能力,开发人员可以在这个设计器的基础上添加新的特性,使得设计器能显示新样式的文档视图。并且加载和保存文档时也能处理新的文档结构。
  7. 若需要可以支持VBA脚本,用户可以编写VBA脚本来控制设计器,包括其设计的文档内容。
      对于计算机程序,后台决定前台,而设计器的后台就是文档对象模型。我们在WEB页面中使用JAVASCRIPT脚本时就是访问了HTML文档对象模型,我们操作XML文档就是访问XML文档对象模型。
      W3C 国际组织对文档对象模型是这样定义的(摘自 http://www.w3.org/DOM/
    The Document Object Model is a platform- and language-neutral interface that will allow programs and scripts to dynamically access and update the content, structure and style of documents. The document can be further processed and the results of that processing can be incorporated back into the presented page. This is an overview of DOM-related materials here at W3C and around the web.
     文档对象模型是一种语言中立的接口或平台,程序或脚本能利用它来访问和更新结构化的文档。这些文档可以被进一步的处理,处理结果可以组成一个有效页面。这是W3C对web上的对文档对象模型原理的一般看法。
    对于编程,文档对象模型其主要内容就是,面对比较复杂的文档,使用面向对象的编程思想,使用一个个程序世界中的对象来映射文档中的每一个特定的部分。加载文档时,可以解析文档,并把其表示的内容映射为一个个对象,此时应用程序可以修改这些对象的数据,当保存文档时,可以将这些对象数据组织起来按照特定的格式保存到文档中。这样程序就通过访问文档对象来访问文档,也可以修改文档对象来修改文档,如此实现了对复杂文档的处理。文档对象模型是处理复杂文档的标准操作模式。
      设计器处理的是复杂的文档,因此也需要使用文档对象模型。文档对象模型可分为三大部分:文档基本元素,文档对象和各种类型的从文档基本元素派生出的文档元素。
    文档基本元素是整个文档对象模型的最基础的对象(就像Object类型是.NET对象集团的基础一样),它定义了文档元素的通用接口,一般定义为抽象类,类型名称可以为DesignElement 。
    文档对象是文档对象模型的顶级对象,它包含了整个文档的内容,其类型名称可以为 DesignDocument 。
    各种类型的文档元素,它是派生自文档基本元素类型,用于描述文档中各种实际存在的元素。其中可以定义一种文档元素,它们可以容纳其他的文档元素,这些元素就是容器元素。实际上文档对象就是最大的容器元素。由于文档对象模型中存在容器元素,因此所有的对象都组成一个树状结构,称为文档对象树,其中根节点就是文档对象。各种文档元素是文档对象模型的活跃分子,扩展文档对象模型大部分工作就是扩展这些文档元素,扩展文档元素需要扩展它们的两个功能,一个是文档的加载和保存,一个就是文档本身保存的数据。
    文档对象模型可以和用户界面相关,也可以不相关,例如XML文档对象模型是无用户界面的。设计器的文档对象模型是和用户界面相关的,对此,扩展设计文档对象模型的文档元素时还需要扩展它们的绘制图形的能力以便设计器能绘制新型的文档元素图形。
    对于设计文档对象模型,其文档基础元素可以定义的内容有三个方面,文档的加载和保存,用户界面相关的接口,维护文档对象树的接口。
    设计文档可以保存为二进制文档,纯文本文档和其他格式,在此推荐使用XML文档格式。其好处是
  1. 设计文档对象模型和XML文档对象模型都属于文档对象模型,两者原理和结构上都有着很大的相似性,设计文档元素和XML文档元素可以存在一一对应的关系。因此使用XML文档加载和保存设计文档对象是很自然的,实现起来比较简单。
  2. XML文档是国际标准的文档格式,非常开方,其他应用程序很容易利用设计器生成的文件,简化了设计器和其他应用系统的数据接口。
  3. 已经存在标准的XML文档解析器和XML文档对象模型,因此不需自己处理XML文档,只需调用标准库加载XML文档对象模型,然后按照一一对应的关系来生成设计文档对象模型。
  4. 使用XML文档有利于保持设计器的各个版本间的兼容性。只要XML文档结构不发生大变化,低版本的设计器可以加载高版本的设计器生成的文档,同样高版本的设计器也很容易加载低版本的设计器生成的文档。若使用二进制文件格式,则设计器需要编写对于不同版本的设计文档的预处理器,比较麻烦而且很难做到向上兼容。
       在保存对象数据到XML文档时,保存方式有两种,保存到XML属性和保存到XML元素。当指定某个XML元素用于保存对象数据时,若使用保存到XML属性时,会对对象每一个属性,将其数据保存到指定名称的XML属性中,而保存到XML元素时,会在当前的XML节点下新增一个指定名称的XML子元素。然后将属性值保存到XML子元素中。这两种方式生成的XML片断为
<element attributename1="value1" attributename2="value2" />
<element >
    <attributename1>value1</attributename1>
    <attributename2>value2</attributename2>
</element>
    面对这两种方式,建议选择第二种,其原因有
  1. 若保存到XML属性,则当对象属性比较多是,使用缩进方式输出的XML文档将比较宽,在查看是会出现横向滚动条,不利于阅读。而保存到XML元素时,XML文档不会很宽,便于阅读。
  2. 若多行文本保存到XML属性,则一般不会以多行文本的方式保存,不利于阅读。而保存到XML元素时,则保存的文本和实际的文本比较接近,便于阅读。
  3. 若保存到XML属性,则保存方式只能是一个属性字符串,而保存到XML元素时则保存的方式很容易进行扩展。
  4. 虽然保存到XML属性方式生成的XML文档比保存为XML元素的方式要小,但XML文档格式的设计目标是方便保存数据和交换数据,而不在乎文档是否冗余,因此我们选择保存方式时不必在乎XML文档的大小。而且一般设计文档的内容不很多,以目前计算机硬件条件无须在意XML文档大小。
      当设计器从XML文档加载设计文档时, 首先生成XML文档对象树, 然后根据一一对应的关系来生成设计文档对象树,此时需要从XML元素保存的信息来判断该XML元素是对应于那种设计文档元素,设计器可以从XML元素名称来判断,也可以从某个XML属性来判断,在此使用XML元素名称来判断,首先是针对一个XML元素,获得其名称比获得某个属性值要方便,其次是XML名称是必然存在的,肯定不为空,而XML属性则可能由于某种原因而缺失,XML名称比XML属性要稳定。
      基于上述的认识,当采用XML文档作为保存方式时,设计基础元素需要定义两个虚函数,一个用于从XML文档加载对象属性数据,另一个要向XML文档保存对象数据。而其他文档元素对象则根据需要重载这两个函数来实现自己的加载和保存对象属性的操作,对于容器元素,还需要保存子元素数据到XML文档和从XML文档加载子元素。当然在实际应用中还要根据需要定义一些辅助成员来帮助加载和保存XML文档。
      设计器生成的XML文档一般保存为文件形式,当然可以根据需要来保存的数据库里或者上传到各种服务器中。若直接保存到数据库中,则整个应用系统中所有的设计器编辑的都是同一个文档版本,而且一旦保存便可立即应用。
      设计器需要绘制文档视图,则需要设计文档对象模型提供支持。因此文档基本元素需要定义两类通用接口,一个是和绘制文档相关的接口,一个是处理鼠标键盘事件相关的接口。
      大部分文档元素需要在文档视图中绘制内容,因此它们需要重载绘制文档的接口,这类接口主要有两个函数,一个是计算元素大小的函数,一般命名为 RefreshSize , 一个是绘制元素的函数,一般命名为RefreshView。
       一般设计者指定元素的大小,元素本身不需要计算其大小,但某些元素可能是根据其内容自动设置大小,因此需要重载计算元素大小的函数RefreshSize来自动设置大小。自动设置大小可能只是设置元素的宽度或高度,也可能是同时设置其宽度和高度。同一个元素,可能在一种状态下不会自动设置大小,而在另外一种状态下需要自动设置大小。所有的这些操作都需要在RefreshSize函数中完成。
      一般的设计元素都需要在文档视图中绘制内容,这时需要重载RefreshView函数,这个函数参数包含了一个System.Drawing.Graphics对象,元素需要使用这个Graphics对象来绘制自己特定的内容,可能是绘制文本,图片或其他图形。
      当所有的文档元素都实现了绘制文档相当的接口,则在设计器的调度下,一个完整的设计文档视图就绘制出来了。而扩展设计器时,若需要指定新显示样式的元素时,需要重载RefreshView和RefreshSize函数来实现新的显示样式,此时扩展的设计器就能显示新样式的文档视图。
     设计器中主要处理鼠标事件,文档基础元素可以定义一些处理鼠标事件的虚函数,名称可以为 HandleMouseDown , HandleMouseMove 和 HandleMouseUp 。
     为了方便文档元素处理鼠标坐标,设计器在调用文档元素的HandleMouse函数时,首先将鼠标光标坐标进行转换,要将鼠标光标在视图区域中的坐标转换为文档元素内部的相对坐标,即相对于元素左上角的相对坐标。
      设计器要依靠鼠标事件来实现设计元素的拖拽操作以实现互换式设计体验。关于鼠标拖拽操作典型的应用就是使用8个控制点来编辑元素边界。当一个元素边界是矩形时,会在元素的边界矩形的四个角和四个边的中点上分布8个控制点,当鼠标移动到这8个点时会修改鼠标光标样式,当鼠标光标在某个控制点上时,用户按下鼠标按键则开始进行鼠标拖拽操作,拖拽时会显示一个虚线绘制的边框,当松开鼠标按键则拖拽操作结束,此时设计器修改拖拽的元素的矩形边界。
      某些文档元素并不进行标准的鼠标拖拽操作,例如对于容器元素,其内部的鼠标拖拽不移动对象而是画出一个选择矩形来选择若干个子对象;对于表格元素,它的表格线上的鼠标拖拽操作是修改表格行的高度和表格列的宽度;而对于线段则是修改端点位置。
      当用户不小心按下鼠标按键,或只是选择某个元素而并不想进行鼠标拖拽操作,此时可以使用一个参数 System.Windows.Forms.SystemInformation.DragSize 来判断是否进行鼠标拖拽。当鼠标按键按下时,设计器就锁定鼠标,若鼠标按键按下后鼠标移动距离超出了 DragSize 的范围时,则表示用户是想进行鼠标拖拽操作的,此时开始真正的鼠标拖拽操作。若鼠标按键从按下到松开时鼠标移动距离始终没超出 DragSize 的范围,则表示用户没有进行鼠标拖拽操作的意图。这样的判断可以让设计器容忍用户的一些误操作。
      设计器还要处理鼠标双击事件处理,对于某些包含文本的元素,用户双击该元素,则在设计视图中显示个文本输入框来直接编辑对象的文本内容。可以定义一个接口 ILabelEditable , 当用户双击某个元素,设计器发现该元素实现了 ILabelEditable 接口,则在设计视图中动态的显示一个文本输入框,然后调用该接口的成员来直接编辑对象文本内容。
      文档基础元素要定义不少接口来用于维护文档对象树。要定义 OwnerDocument 属性来指定元素所在的文档对象,要定义 Parent 属性来指明元素的父节点,定义 Items 属性来指明该元素的子元素列表。对于容器元素,还要维护它的子元素列表。
      设计文档对象作为文档树的根节点,担负着维护整个对象树的重任,包括文档整体的加载保存,文档整体的绘制,遍历整个对象树结构入口,还要为脚本提供接口。它是访问文档对象树的入口点。
    可以从文档基础元素上派生一些比较基础的文档元素类型。这些比较基础的文档元素类型可以包括
  • 矩形元素基础类型,类型名称为DesignRectangleElement , 设计文档中大部分元素的边界是矩形,因此定义矩形元素基础类型作为这些矩形类型的元素的共同基础。矩形元素基础类型实现了使用8个点的控制点来修改元素位置和大小的能力,鼠标在对象边界只那的鼠标拖拽操作就可移动元素位置。此外还定义了内容和边界之间的边距信息。
  • 线段类型,类型名称为DesignLineElement, 设计文档某些元素是以线段方式显示的,因此定义线段类型作为这些元素类型的基础类,线段类型定义了两个端点的位置,线段的显示样式,标签文档等信息。此外还重载了鼠标事件,使得用户可以使用鼠标拖拽线段的两个端点来修改线段端点的坐标。此外还要重载命中操作,用于判断某个坐标是否命中线段对象,若指定点距离线段的垂直距离小于某个参数,和点在线段某个端点上的拖拽点中则命中线段,否则没命中。
  • 容器元素类型,类型名称为DesignElementContainer , 该元素可以包含若干个子元素,它是从DesignRectangleElement 派生的,因此它的边界是矩形。鼠标在容器中的拖拽操作不是移动容器,而是动态绘制一个选择矩形,当完成拖拽操作时,就根据这个选择矩形来设置子元素的选中状态。根据选择矩形来选择子元素有两种方式,一种是,若子元素边界和选择矩形粘边就被选中,另一种是,若子元素完全在选择矩形内部时才被选中。容器元素在绘制子元素时就执行矩形覆盖操作后再调用子元素的RefreshView成员。
  • 带标题容器元素,类型名称为 DesignCaptionContainer , 该元素派生自容器元素,可以包含若干个子元素,但它顶端有个标题栏,可以显示文本,用户使用鼠标拖拽这个标题栏可以修改元素的位置。此外它还实现了 ILabelEditable 接口,当用户双击标题栏时可以直接编辑标题栏文本。
  • 文本元素,类型名称为DesignTextElement,很多文档元素只是简单的显示文本内容,则定义文本元素作为这些简单显示文本内容的元素的共同基础。它派生自DesignRectangleElement , 此外还实现了 ILabelEditable 接口用于直接编辑文本内容。此外还支持文本输出角度控制,此时绘制文本时将以元素中心为原点旋转任意角度进行绘制。文本元素绘制带角度的文本时需要临时修改图形绘制对象Graphics的转换矩阵来设置绘制角度。
  • 增强文本元素,类型名称为DesignTextElementExt , 该元素派生自DesignTextElement, 对文本输出格式进行了强化,它支持行间距和字符间距,此外还进行了文本右边缘的对齐操作。显示对于大段文本时,尤其包含中文字符和英文字符,某些程序没有进行文本右边缘对齐操作。例如IE,记事本等,这是因为中文字符和英文字符宽度不一样。每一行文本的内容宽度由于中英字符的个数不一样,很容易导致文本宽度不一样,因此当文本左边缘对齐时,其右边缘很可能是参差不齐的。但MS Word显示大段文本时它的文本左右边缘都是对齐的,它通过在显示文本时插入额外的用户难以察觉字符间距来修正文本显示宽度。增强型文本元素就利用了这个原理来实现文本右边缘对齐。
  • 表格元素,类型名称为DesignTableElement,表格元素是一种复杂的容器元素,它包含表格行(DesignTableRowElement),表格列(DesignTableColumnElemetn)和单元格(DesignTableCellElement)对象,其中单元格可以进行横向合并和纵向合并。而表格包含的表格行和单元格也是容器元素,用户不能直接修改单元格的大小位置,而只能调解表格行的高度和表格列的宽度来修改单元格的大小位置。单元格也是容器元素,因此单元格内可以放置若干个子元素。在很多情况下单元格只是显示简单的文本内容,因此单元格定义了一些用于显示文本内容的属性,此外实现了 ILabelEditable 接口来方便直接编辑单元格文本内容。
  • 图片元素,类型名称为 DesignImageElement , 它是从DesignRectangleElement 派生的,用于简单的显示一个图片。由于图片对象(System.Drawing.Image)使用了未托管资源,因此图片元素实现了 System.IDisposable 接口。
  • 此外还定义了一些元素,这些元素可以模拟绘制Windows基础控件,包括文本标签,按钮,单选框,复选框,文本框,列表,下列列表,组合框,进度条和窗体。可以根据这些元素来很容易的模拟出一个窗体设计器。
    设计器的主要工作之一就是绘制文档视图。其绘制过程一般是
  1. 设计器控件重载它的OnPaint成员或绑定Paint事件。
  2. 当操作系统需要重新绘制设计器控件时会触发它的Paint事件。
  3. 设计器获得绘制图形使用的System.Drawing.Graphics对象和一个表示绘制区域的剪切矩形ClipRectangle,然后将其作为参数调用文档对象的RefreshView函数。
  4. 文档对象进行一个初始化工作,然后遍历对象树结构,找到所有和剪切矩形粘边的文档元素,调用它们的RefreshView函数,让各个元素绘制各自内容。
  5. 当所有工作完毕,则文档视图绘制完毕。
      设计器绘制文档是遇到一个难题就是闪烁,当用户滚动视图和更新视图时,用户界面很容易出现闪烁,过多的闪烁会比较严重的影响使用者的使用。由于设计文档是比较复杂的文档,绘制整个文档视图工作量大,绘制时间长,因此需要采用各种优化来减少绘制时间,减少闪烁。
      对于闪烁有一个算是一劳永逸的办法就是使用双缓冲技术。在绘制图形时,首先将图形绘制到一个内存中的BMP图片上,然后将这个BMP图片绘制到用户界面上。这种方法可以最大程度的减少闪烁,而且在.NET中使用双缓冲也比较简单。所以不大使用双缓冲技术,有两个原因
  1. 双缓冲实际上增加了整个绘制文档的工作量,延长了绘图时间。当用户滚动视图时,会造成视图很“沉重”的感觉,用户界面响应迟钝。
  2. 双缓冲掩盖了程序的不足之处。开发人员可以根据闪烁程度来判断绘图操作是否需要优化,以及优化效果。但双缓冲消灭了闪烁,开发人员也就没有优化绘图操作的迫切需求,助长了开发人员的懒惰。程序绘制图形时缓慢不堪,而很难从表面看出问题的可能原因。
    其实可以这样,设计器在开发时不使用双缓冲,但发布时则使用双缓冲。
    由于设计器采用直角坐标方式,因此各个元素间存在相互覆盖的关系,当存在大面积的覆盖时,绘制文档时必需要针对这种情况进行优化处理来提高绘制文档的速度,减少计算机屏幕闪烁。针对覆盖现象而进行优化时可以进行矩形覆盖操作,对于矩形覆盖操作。设计器绘制某个元素时,首先针对这个元素进行矩形覆盖运算,将运算结果作为RefreshView函数的某个参数来传入,当文档元素内容比较多时,可以根据这个矩形覆盖运算结果来减少绘制量,提高绘制速度。
 设计视图还应提供缩放显示功能,可以放大设计视图来更清楚的显示细节,可以缩小设计视图来总体的把握整个文档。GDI+有个转换矩阵,可以很容易的实现设计视图的缩放显示。但此时所有的鼠标坐标数据都得进行相应的缩放处理。
      设计视图控件是设计器在用户界面上的展示接口。它是一个标准的Windows控件,该控件派生自System.Windows.Form.UserControl。用户使用鼠标和键盘在这个控件里面编辑文档,它重载了OnMouseDown , OnMouseMove 和 OnMouseUp 成员,对鼠标消息进行了一下包装后供设计文档对象使用。重载了OnPaint 成员来更新文档视图。重载了 OnDoubleClick 来进行试图直接编辑文档元素的文本内容。
   当用户设置某个元素为当前元素,则设计视图控件将根据需要来进行滚动以便当前元素出现在可视区域中。若当前元素大小小于可视区域大小,则处理比较简单,只要根据可视区域大小和元素在视图中的位置就可计算滚动位置。若元素宽度或高度大于可视区域的宽度和高度,则需要进行额外的判断,以避免滚动时发生跳跃。
一个好的设计器应当支持鹰眼技术,所谓鹰眼,通俗的讲就是小地图,它一般放置在程序界面的某个角落,它的面积不大,主要功能是让人瞥上一眼就能大体了解整个文档的结构,并能通过鼠标点击快速的滚动文档。
所谓鹰眼,通俗的讲就是小地图,玩过星际争霸的都知道,它的主界面左下角就是小地图,它的主要功能是让人瞥上一眼就能大体了解整个的战场的地形和敌我力量分布,而且能利用小地图在整个战场上快速的移动视角,因此在很多游戏中鹰眼的作用很大。
在GUI程序中,鹰眼也是类似的作用。它能显示全部文档的大概的结构,而且能利用它来快速的在文档中移动视角。很多商业的GUI程序,特别是设计器类的程序,都提供了鹰眼功能。
鹰眼的结构一般是在一个显示区域的中间,显示了整个文档的缩小的视图,一般的为了性能,采用整个文档的缩小的预览图片来显示。然后在这个小视图上使用一个方框或者其他手段来突出显示其中的一部分,而这一部分代表了当前主视图区域在整个文档中的位置和大小。当主视图区域进行滚动时,鹰眼中突出显示的区域也会移动。当用户用鼠标在鹰眼区域中点击或拖拽方式改变突出显示区域的位置时,主视图区也会做相应的滚动。当整个文档的内容发生大的改变时,鹰眼中文档预览BMP图片也会更新。
实现鹰眼的过程,首先准备文档预览BMP图片,该图片要求大小合适,不能过大或过小。根据鹰眼区域的大小和整个文档从长宽比获得合适的图片大小,算出缩小比例,这个比例很重要,需要妥善保存。然后创建一个 System.Drawing.Bitmap 对象,再使用System.Drawing.Graphics.FromImage 创建一个Graphics 对象,调用 Graphics.ScaleTransfrom 来设置缩小比例,然后使用个Graphics绘制整个文档内容,绘制过程和正常在视图区域中绘制没什么区别。为了提高性能,当缩小比例很小时,有些比较小的文档元素不必绘出,也不必绘制文本,此时需要靠缩小比例来进行判断了。
当用户改变的文档内容时,应该考虑更新鹰眼的预览BMP图片了,当改变量相对于整个文档来说不大时,不必更新,若改变量比较大时需要更新预览BMP图片了,为了提高性能,每一次改变文档时都将改变区域的面积和整个文档的面积进行比较,若两者比率超过某个设定的值时,就进行更新,否则不必更新。
获得预览BMP图片后,将其放置在鹰眼区域的中间。然后根据主视图区域的大小( Control.ClientSize )和预览图片的缩小率获得其在鹰眼中的映射区域的大小,根据主视图区域在整个文档中的位置获得其映射位置在预览图片中的位置,如此就可确定主视图区域在预览图片中的映射区域的位置和大小,每次重绘鹰眼区域时就可以突出显示映射区域了。主视图区域每次进行滚动后都需要重新计算映射区域的位置并立即重绘鹰眼区域。若仅简单的使用
Control.Invalidate声明鹰眼区域无效时会导致更新不及时,影响效果。由于鹰眼区域不大,绘制操作简单,在此简单的使用Control.Refresh进行全部绘制。
由于映射区域的大小是基于主视图区域的客户区大小的,因此当主视图区域大小发生改变时也需要更新鹰眼,可以在主视图区域的Resize事件中进行处理。
在GDesigner中,本人不是使用整个文档的大小来计算映射区域的大小,而是使用文档中包含所有内容的外接矩形来计算映射区域的大小,没什么很特别的原因,个人做法而已。
.NET的UserControl没有提供滚动事件,因此需要重载它的WnProc成员方法来进行增强,当WnProc方法传入的参数表示滚动消息时就触发一个滚动事件,而主程序就可响应这个事件来更新鹰眼了。
当用户在鹰眼中用鼠标点击或托拽操作来改变突出显示区域时,根据突出显示区域的位置和预览图片缩小率算出主视图区域在整个文档中的位置,然后调整主视图区域的滚动位置即可(使用属性 AutoScrollPosition )。在GDesigner中,当用户在预览图片中鼠标按下时,程序会移动映射区域位置使得鼠标按下时的位置为映射区域的中心,然后调整映射区域使之全部在预览图片中。然后滚动主视图区域。当鼠标按下后的鼠标移动事件中,鼠标移动多少则映射区域移动多少,然后相应的滚动主视图区域。
当主视图区域本身进行了缩放处理,则此时主视图区域和鹰眼中映射区域间会有两个缩放比率,此时坐标转换则多了一次处理。       
在Windows图形化用户界面编程中,若程序自己绘制用户界面时,会经常碰到界面闪烁,比如其他窗口在上面移动,用户界面滚动,这些都有可能导致闪烁。在一个容器中绘制特定的文档,需要相应作为绘图容器的控件的OnPaint事件,需要在OnPaint事件处理中重新绘制文档,而Windows操作系统一般会在两种情况下触发OnPaint事件:容器控件被其他窗体覆盖后又显示,还有就是容器控件的滚动处理。在这些情况下,Windows操作系统会频繁的触发OnPaint事件,而应用程序会频繁的在绘图容器中重新绘制图形,若应用程序没有进行很好的优化,则很有可能导致用户界面闪烁。
用户界面出现闪烁自然害处多多,首先它使得你的程序看起来不专业,甚至有不稳定的嫌疑,对于追求完美的你这么会容许它的存在呢;其次闪烁会损害用户的视力,容易让用户产生视觉疲劳。
首先说说闪烁的本质,说到本质,就不得不提一些计算机系统结构和Windows图形用户子系统的一些知识。我们知道,在计算机内存中有一个区域叫做显存,而显卡则每过一些毫秒就从扫描显存,然后根据操作显示器来绘制一个个象素,因此每过一些毫秒显示器显示的内容就会重新设置一遍,由于这是硬件操作,非常快,若画面内容没有变化,则人类肉眼是看不到这个刷新的,此时用户界面是没有任何闪烁。

下图就是应用程序绘制用户界面的原理,应用程序在CPU的支持下向显存填充数据,而以此同时显卡也从显存加载数据操作显示器绘制图形,而用户界面闪烁也就根源于这种显示结构。前面提到,显卡每过一些毫秒就会扫描显存,刷新显示器的显示。假设有个显示卡,设置其刷新频率为50赫兹,则它每20毫秒就扫描显存刷新显示器,而显卡的操作和应用程序的操作和应用程序的操作之间没有任何关系,显卡是自带处理器的,于是应用程序和显卡这两个对象同时操作显存,显卡只读取显存,而应用程序则修改显存,这就导致了类似多线程程序的数据同步的问题了。但这时硬件结构决定此时没有什么锁定机制可使用。显卡每20毫秒就进行刷新操作,连操作系统也挡不住,而且应用程序根本不知道显卡会何时进行刷新操作。

 

某个时刻,应用程序需要绘制用户界面,首先需要清空绘制容器,因此将显存一大片区域设置为白色,应用程序刚完成了清空操作,还每来得及绘制内容时,显卡就刷新了,很快显示器上显示了一大片白色。同时,应用程序开始绘制内容,应用程序运行缓慢,它化了20毫秒绘制了文档的上半身,文档上半身主要为红色,刚绘制了上半身,显卡就好不留情的进行刷新,很快显示器上显示了一半的文档,刚才一半的白色大半变成了红色,此时用户看来,显示器一下变成一片白,然后很快一半变成红色,此时显示器内容产生了两次大面积的内容变幻,然后应用程序又化了20毫秒显示了文档的下半身,文档下半身主要为绿色,此时显卡进行刷新,显示器上另一半还残存的白色又变成绿色。由于应用程序绘制文档完毕,因此不再修改显存,此时显示器的显示的内容不再发生改变。
在上面的描述中,显示器首先从花花绿绿变成一片白,20毫秒后一半变成红色,又20毫秒后另一半变成绿色,如此大面积的显示内容短期的改变就是所谓的闪烁,此时计算机显示器就是在折磨用户的眼睛。
其实从广义上说,计算机显示器显示的内容频繁的发生大面积的改变都是考验人类的眼睛。有些人玩一些激烈的3D游戏,比如雷神CS之类的,若玩的时间长点眼睛就受不了。就是因为这些游戏程序导致显示器显示的内容频繁的发生大面积的改变。因此游戏程序也算导致用户界面闪烁。只不过这种闪烁不算难受。
通过上面的讨论,知道了闪烁的根源,于是我们发现了问题,并分析了问题,现在解决问题。在目前的计算机结构中,我们的应用程序只能通过填充显存来绘制用户界面,在这种情况下,对付闪烁的不二法则就是快,应用程序要非常快的修改显存。对于每20毫秒进行刷新操作的显卡,若我们的应用程序能在20毫秒内修改显存完毕,则在很多情况下显卡就只会导致显示器显示的内容发生改变,减少闪烁,应用程序填充显存前后显存的数据进行对比,若数据前后不一致的字节数越少,显示器中刷新操作前后颜色发生改变的象素数就越少,这用户界面的闪烁就越小。
提高应用程序修改显存的速度的方法有很多中,而且Windows操作系统为我们做了许多底层的操作。我们知道若一个窗体被覆盖后又显示了,则Windows操作系统就会向该窗体发送重绘消息,而且还会传一个矩形数据,该矩形表示窗体中需要刷新的区域,应用程序可以根据这个矩形来重新绘制文档的某个部分,这样就不必要绘制所有的内容,提高绘制速度,减少绘制时间,这就需要进行绘图代码的优化。
在某些情况下,绘图速度很难优化起来,此时可以采用所谓“双缓冲”的技术来减少闪烁,应用程序可以在内存中创建一个和屏幕相兼容的图形设备上下文。该上下文实际上处理一个保存在内存中的BMP图片对象,这样就容许应用程序缓慢地在这个BMP上绘制图形。应用程序绘制完毕,就用WIN32API函数BitBlt来将BMP图片填充到显存中,BitBlt函数速度非常快,足以在屏幕的刷新周期内完成绘图,这样能基本上避免闪烁。
俗话说,说得容量做到难,编程也一样,本文中对付闪烁的说的轻巧,但在实际编程中,闪烁一直是图形化用户界面编程的老大难问题,需要精心的设计程序结构,优化代码,提高速度。这需要靠很多的理论知识和深厚的编程功底,这些需要长期的编程实践。
这段代码能演示图形化用户界面编辑中的一种优化手段,使用VS.NET创建一个C#的标准Wi32程序
using System;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms;
namespace ViewPicture
{
        public class frmViewImg : System.Windows.Forms.Form
        {
               private System.Windows.Forms.Button cmdLoad;
               private System.Windows.Forms.Label label1;
               private System.Windows.Forms.Panel panel1;
               private System.Windows.Forms.Label label2;
               private System.Windows.Forms.Panel panel2;
               private System.ComponentModel.Container components = null;
 
               public frmViewImg()
               {
                       InitializeComponent();
               }
 
               protected override void Dispose( bool disposing )
               {
                       if( disposing )
                       {
                               if (components != null)
                               {
                                      components.Dispose();
                               }
                       }
                       base.Dispose( disposing );
               }
               private void InitializeComponent()
               {
                       this.cmdLoad = new System.Windows.Forms.Button();
                       this.label1 = new System.Windows.Forms.Label();
                       this.panel1 = new System.Windows.Forms.Panel();
                       this.label2 = new System.Windows.Forms.Label();
                       this.panel2 = new System.Windows.Forms.Panel();
                       this.SuspendLayout();
                       this.cmdLoad.Location = new System.Drawing.Point(16, 8);
                       this.cmdLoad.Name = "cmdLoad";
                       this.cmdLoad.Size = new System.Drawing.Size(208, 32);
                       this.cmdLoad.TabIndex = 0;
                       this.cmdLoad.Text = " 打开图片文件(最好图片要大)" ;
                       this.cmdLoad.Click += new System.EventHandler(this.cmdLoad_Click);
                       this.label1.AutoSize = true;
                       this.label1.Location = new System.Drawing.Point(16, 48);
                       this.label1.Name = "label1";
                       this.label1.Size = new System.Drawing.Size(42, 17);
                       this.label1.TabIndex = 1;
                       this.label1.Text = " 未优化" ;
                       this.panel1.AutoScroll = true;
                       this.panel1.BackColor = System.Drawing.SystemColors.Window;
                       this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
                       this.panel1.Location = new System.Drawing.Point(8, 72);
                       this.panel1.Name = "panel1";
                       this.panel1.Size = new System.Drawing.Size(696, 224);
                       this.panel1.TabIndex = 2;
                       this.panel1.Paint += new System.Windows.Forms.PaintEventHandler(this.panel1_Paint);
                       this.label2.AutoSize = true;
                       this.label2.Location = new System.Drawing.Point(16, 312);
                       this.label2.Name = "label2";
                       this.label2.Size = new System.Drawing.Size(29, 17);
                       this.label2.TabIndex = 1;
                       this.label2.Text = " 优化" ;
                       this.panel2.AutoScroll = true;
                       this.panel2.BackColor = System.Drawing.SystemColors.Window;
                       this.panel2.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
                       this.panel2.Location = new System.Drawing.Point(8, 336);
                       this.panel2.Name = "panel2";
                       this.panel2.Size = new System.Drawing.Size(696, 224);
                       this.panel2.TabIndex = 2;
                       this.panel2.Paint += new System.Windows.Forms.PaintEventHandler(this.panel2_Paint);
                       this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);
                       this.ClientSize = new System.Drawing.Size(720, 573);
                       this.Controls.Add(this.panel1);
                       this.Controls.Add(this.label1);
                       this.Controls.Add(this.cmdLoad);
                       this.Controls.Add(this.label2);
                       this.Controls.Add(this.panel2);
                       this.Name = "frmViewImg";
                       this.Text = " 显示图片" ;
                       this.Resize += new System.EventHandler(this.frmViewImg_Resize);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值