SCRIBBLE 示例:MFC MDI 绘图应用程序
DRAWCLI 示例:阐释活动容器支持与特定于应用程序的功能的集成
JTDraw www.codeguru.com
它定义了CDrawObj和CDrawTool两个基类,各种图形分别继承与两个类,画图的时候根据不同的ID,使用不同的工具(多态),而工具又和它的某个CDrawXXX关联...好好看看,我觉得MS还是比较牛的想到这个办法...Jtdraw其实就是这个改了改
定义了几个变量:
static CSelectTool selectTool;
static CRectTool lineTool(line);
static CRectTool rectTool(rect);
static CRectTool roundRectTool(roundRect);
static CRectTool ellipseTool(ellipse);
static CPolyTool polyTool;
当选中工具后,view中的事件onlbButtondown()和onmousemove()触发,调用类CDRAWTOOL中的相应的事件
分为CSelectTool和CDrawTool两种
CRectTool和CPolyTool继承CDrawTool
一般情况下用CSelectTool
当要画矩形的时候CRectTool
当要画折线的时候CPolyTool
看对应的Tool的OnLButtonDown和OnMouseMove
CDrawTool负责响应鼠标事件。CDrawRect是由CRectTool来完成具体的绘图的。
大概的过程如下:
CDrawTool的OnLButtonDown是一个虚函数,派生类都重写了这个函数。
CDrawTool类有一个静态成员,是一个链表,保存了各种派生类的指针
还有一个静态函数FindTool,用来返回当前选中的派生类指针,不过返回的是一个CDrawTool *。
在View的OnLButtonDown中调用FindTool来获得这个CDrawTool *指针(暂且名叫pTool)。然后调用pTool-> OnLButtonDown
这时候,通过多态便会导航到派生类的OnLButtonDown
用OOP的思想,利用MFC类库的机制,设计如下一个图元类库:
CGraphBaseObj : public CObject
{
CGraphBaseObj();
CGraphBaseObj();
// serialize
virtual void Serialize(CArchive& ar);
virtual void SetSelect(BOOL bSel=TRUE);
virtual BOOL IsSelected();
virtual BOOL Move(CPoint point);
virtual BOOL Size(CSize size);
virtual BOOL OnLButtonDown(CWnd* pWnd, UINT nFlags, CPoint point);
virtual BOOL OnMouseMove(CWnd* pWnd, UINT nFlags, CPoint point);
virtual BOOL Draw(CDC* pDC);
}
在子类CGraphRectObj、CGraphLineObj、CGraphTriangleObj、CGraphStarObj、
CGraphEllipseObj中分别实现类中定义的虚函数。
实现的功能主要包括:
(1). 图元绘制 根据当前选中的绘图工具(直线、椭圆、矩形),使用鼠标直接在View的Client区域内,将图元一笔绘制出来。
(2). 图元操作 包括图元的选择、移动、缩放、删除。
在CodeProject上一位高人用C#模仿了DRAWCLI
C#2.0中controlpaint 可以画出c#自带的控件
Rectangle bound = this.ClientRectangle();
using (Pen pen = new Pen(Color.Red)
{
{System.Windows.Forms.ControlPaint.DrawButton(g, bound, ButtonState.Normal);
}
DrawCli代码中使用了双缓冲和裁剪区技术以及坐标变换等技术。
DrawCli中2个主要绘图相关的函数: OnDraw和OnPrepareDC。
OnPreparDC的作用是设置坐标的映射方式和窗口原点。由于程序使用了MM_ANISOTROPIC 映射方式,同时设置了X的正方向向右,Y轴的正方向向上。
为了便于分析理解,我们可以改造一下代码,设置程序使用MM_TEXT映射模式。
新建一个新的工程,视图从CScrollView派生。
对OnPrePareDc函数中添加下面代码,设置映射模式为MM_TEXT模式
pDC->SetMapMode(MM_TEXT);
pDC->SetWindowOrg(0,0);
为了便于理解和分析,我们在OnDraw函数中绘制一个矩形。
在OnDraw函数中添加下面的代码。
CDCdc;
CDC*pDrawDC = pDC;
CBitmap bitmap;
CBitmap* pOldBitmap;
CRectclient;
pDC->GetClipBox(client);
CRectrect = client;
DocToClient(rect);
if(dc.CreateCompatibleDC(pDC))
{
if(bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()))
{
OnPrepareDC(&dc, NULL);
pDrawDC = &dc;
// offset origin more because bitmap is justpiece of the whole drawing
CPointptOrg = dc.GetViewportOrg();
dc.OffsetViewportOrg(-rect.left, -rect.top);
ptOrg= dc.GetViewportOrg();
pOldBitmap = dc.SelectObject(&bitmap);
dc.SetBrushOrg(rect.left % 8, rect.top % 8);
// might as well clip to the same rectangle
dc.IntersectClipRect(client);
}
}
// paint background
CBrushbrush;
if(!brush.CreateSolidBrush(RGB(0,255,0)))
return;
brush.UnrealizeObject();
pDrawDC->FillRect(client, &brush);
CBrushbrush1;
if(!brush1.CreateSolidBrush(RGB(255,0,0)))
return;
CPenpen;
if(!pen.CreatePen(PS_SOLID,1,RGB(0,0,0)))
return;
CBrush*pOldBrush;
CPen*pOldPen;
pOldBrush = pDrawDC->SelectObject(&brush1);
pOldPen = pDrawDC->SelectObject(&pen);
CRectrect1(0,0,100,100);
dc.Rectangle(rect1);
if(pDrawDC != pDC){
pDC->SetViewportOrg(0, 0);
pDC->SetWindowOrg(0,0);
pDC->SetMapMode(MM_TEXT);
dc.SetViewportOrg(0, 0);
dc.SetWindowOrg(0,0);
dc.SetMapMode(MM_TEXT);
pDC->BitBlt(rect.left,rect.top, rect.Width(),rect.Height(),
&dc,0, 0, SRCCOPY); // 把dc的位图拷贝到pDC环境中
dc.SelectObject(pOldBitmap);
dc.SelectObject(pOldBrush);
dc.SelectObject(pOldPen);
}
我们首先讲一下双缓冲技术,在上面的代码中,所谓双缓冲就是在VC中进行绘图过程处理时,如果图形刷新很快,经常出现图形闪烁的现象。利用先在内存绘制,然后拷贝到屏幕的办法可以消除屏幕闪烁,具体的方法是先在内存中创建一个与设备兼容的内存设备上下文,也就是开辟一快内存区来作为显示区域,然后在这个内存区进行绘制图形。在绘制完成后利用BitBlt函数把内存的图形直接拷贝到屏幕上即可。
在上面的代码中,dc.CreateCompatibleDC(pDC)和bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height())意思就是创建一个内存兼容DC,创建一个内存兼容位图。然后用pOldBitmap = dc.SelectObject(&bitmap)函数把将位图选入到内存显示设备中,只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上 。
只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。
上面说了这么多,用一个具体的图来加深理解。
当开始显示视图时如下所示:
通过GetClipBox函数获取的窗口大小为:
rect.l = 0,rect.t= 0,rect.r =934,rect.b=434;
当我们滑动上下的滑块时,如下图所示:
上面的黑色矩形就是裁剪区,其大小为:
rect.l = 0,rect.t= 431,rect.r =934,rect.b=434
在程序中使用用pDC->GetClipBox()得到裁剪区,然后在这个区域内进行绘图。这样做,主要是为了提高绘图效率,避免闪烁。
为了便于理解分析,我们已经把DrawCLi程序的原来的映射方式改为MM_TEXT,对于坐标变换,网上有很所资料可以参考,这里不详细讲解,这里只是讲一些主要概念。
1) SetWindowOrg(x, y) 是把设备坐标的原点(视口)映射到逻辑坐标的(X,Y)处
2) SetViewportOrg(x, y) 是把逻辑坐标的原点(窗口)映射到设备坐标的(X,Y)处
3) 设备原点永远是客户区的左上角顶点(upperleft corner of the client area)
4) 设备坐标的X, Y轴方向是固定的,单位也是固定的,X轴向右递增,Y向下递增,单位都是像素
有一个比较好的软件是viewport软件,对加深坐标变换的理解很有好处。
1) SetWindowOrg(x, y) 是把设备坐标的原点(视口)映射到逻辑坐标的(X, Y)处
void CTestViewView::OnDraw(CDC* pDC)
{
CTestViewDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->SetMapMode(MM_TEXT);
pDC->SetWindowOrg(100, 100);
pDC->Rectangle(0, 0, 200, 200);
}
2) SetViewportOrg(x, y) 是把逻辑坐标的原点(窗口)映射到设备坐标的(X, Y)处
void CTestViewView::OnDraw(CDC* pDC)
{
CTestViewDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->SetMapMode(MM_TEXT);
pDC->SetViewportOrg(100, 100);
pDC->SelectStockObject(GRAY_BRUSH);
pDC->Rectangle(0, 0, 200, 200);
}
上面是对坐标变换的直观的理解和介绍。具体到DrawCli程序,主要是理解
dc.OffsetViewportOrg(-rect.left, -rect.top);
pDC->SetViewportOrg(0, 0);
pDC->SetWindowOrg(0,0);
pDC->SetMapMode(MM_TEXT);
dc.SetViewportOrg(0,0);
dc.SetWindowOrg(0,0);
dc.SetMapMode(MM_TEXT);
pDC->BitBlt(rect.left, rect.top, rect.Width(),rect.Height(),
&dc, 0, 0, SRCCOPY);
这三段代码的作用。
我们先分析逻辑坐标的情况:
开始视图显示时,视图在内存DC上的逻辑坐标的坐标为(0,0) (934,434)。
移动滑块后,clip的区域的坐标为(0,434) (934,437) 。clip的区域在屏幕客户区的坐标为:(0,431) (934,434)。
pDrawDC->FillRect(client, &brush)函数在这个客户区坐标上填充背景色,才不会有黑块出现。
dc.OffsetViewportOrg(-rect.left,-rect.top)函数把内存的DC逻辑坐标的原点移动到内存设备坐标的(0,-434)处,clip区域在设备坐标中的坐标为(0,0) (0,3)。
dc.SetViewportOrg(0, 0);
dc.SetWindowOrg(0,0);
dc.SetMapMode(MM_TEXT);
操作后,内存DC的逻辑坐标原点与设备坐标原点重合。
pDC->BitBlt(rect.left, rect.top, rect.Width(),rect.Height(),&dc,0, 0, SRCCOPY);
操作之后,复制的目标区域为(0,431) (934,434),源区域为(0,0)(0,3)。
BitBlt中的源区域的坐标是参考源DC的逻辑坐标系。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
http://www.codeproject.com/csharp/DrawTools.asp(源代码请参见原文)
DrawTools sample shows how to create a Windows Forms application for drawing graphic objects in a Windows client area using mouse and drawing tools. Drawing tools implemented in this sample are: Rectangle, Ellipse, Line, and Pencil. There are well-known techniques for creating such type of applications, like: interaction with mouse, flicker-free drawing, implementing of drawing and selection tools, objects selection, managing of objects Z-order etc. MFC developers may learn all this stuff from MFC sample DRAWCLI. DrawTools C# program reproduces some of DRAWCLI functionality and uses some design decisions from this sample.
介绍
DrawTools示例告诉你怎么创建一个Windows窗体,来使用鼠标和画图工具在窗体上的可用区域画图。这个示例中实现了以下画图工具:矩形,椭圆形,直线和铅笔。其中有一些众所周知的技术来创建这个程序,比如说:鼠标的交互,无闪烁画图,实现了画图和工具选择,物体选择,控制物体的Z轴次序,等等。MFC开发者可以从DRAWCLI.这个MFC示例中了解到所有这些特性。DrawTools这个C#程序复制了一些DRAWCLI的功能,并且在这个例子中使用了一些设计决定。
DrawTools solution contains two projects: DrawTools Windows Forms application and DocToolkit Class Library. DrawTools implements specific application stuff, and DocToolkit contains some standard classes for file managing.
Main features of the DrawTools solution are described below.
DrawTools解决方案包括两个工程:DrawTools窗体应用程序和DocToolkit类库。DrawTools实现了一些特别的应用程序功能,DocToolkit包含有一些用于文件管理的标准类库。
以下描述了DrawTools 解决方案的主要特点:
DocToolkit Library contains set of classes which may be used for creation of document-centric Windows Forms applications. Instances of classes exported from the DocToolkit Library are kept in the main form of the DrawTools project and used for general file-related operations.
Every Windows Forms application has a number of controls like menu items, buttons, toolbar buttons etc. Depending on current situation and user commands, these controls may have different states: enabled/disabled, checked/unchecked, visible/invisible etc. Every user action may change this state. Setting of controls' state in every message handler may be error-prone. Instead of this, it is better to manage controls' state in some function which is called after every user action. MFC has the great ON_UPDATE_COMMAND_UI
feature which allows to update toolbar buttons' state at application idle time. Such a feature may be implemented also in .NET programs.
在程序空闲的时候操作Windows的控件状态
每一个Windows窗体应用程序都会有许多控件,比如说菜单项,按钮,工具栏按钮等等。根据当前状态和用户的命令,这些控件可能有不同的状态:enabled/disabled,checked/unchecked, visible/invisible 等等。用户的每一个操作可能改变这些状态。在每一个消息句柄中改变控件的状态可能导致错误。取而代之的方法是,在用户每一个操作后调用一些函数,在这些函数中管理控件的状态。MFC有一个很好的特性叫ON_UPDATE_COMMAND_UI
,
它允许在应用程序的空闲时间更新工具栏按钮的状态。这个特性也可以在.NET程序中实现。
Consider the situation when user clicks the Rectangle toolbar button. This button should be checked, and previously active tool should be unchecked. Rectangle button message handler doesn't change form controls' state, it just keeps current selection in some variable. Idle message handler selects active tool and unselects inactive tool.
考虑一下这种情形:当用户点击工具栏上的Rectangle按钮,这个按钮就要显示为被选中,前一个激活的工具就要显示为没选中。Rectangle按钮的消息句柄并没有改变窗体控件的状态,它只是在一些变量中保存了当前的选择。空闲的消息句柄选择激活的工具,取消未激活工具的选择。
DrawObject
class has virtual HitTest
function which detects whether point belongs to graphic object:
DrawObject
类有一个叫HitTest
的虚拟函数,用来侦测是否Point属于图形对象。
Derived classes use virtual PointInObject
to make hit test. This function is called from HitTest
. DrawRectangle
class implements this function by a simple way:
继承类使用虚拟的PointInObject
来做点击测试。这个函数调用自HitTest
。DrawRectangle
类使用了一种简单的方法实现了这个函数:
DrawLine
implementation of this function is more complicated:
DrawLine
对这个函数的实现更加复杂:
DrawPolygon
function works by the same way, but AreaPath
contains all lines in the polygon.
DrawPolygon
函数使用了同样的方法,但是AreaPath
包含了多边形的所有线。
GraphicList
class implements ISerializable
interface which allows to make binary serialization of the class object. DrawObject
class has two virtual functions which are used for serialization:
序列化
GraphicList
类实现了ISerializable
接口,这个接口用作类对象的二进制序列化。DrawObject
类有两个虚函数用来做序列化。
These functions are implemented in every derived class. Binary file has the following format:
这些函数在每一个继承类中都实现了。二进制文件有以下格式:
Number of objects
Type name
Object
Type name
Object
...
Type name
Object
This allows to write generic serialization code in the GraphicList class without knowing any details about serialized objects:
这样就可以在GraphicList类里写普通的序列化代码,而不需要了解序列化对象的任何细节。