devExpress的Winform库,是采用的GDI的方式来绘制的.
如果编过WIN32或是MFC的GDI程序,应当一看就明白是怎么回事.
但说归说,做归做.
今天有会闲工夫,分析了一下devExpress 的Scheduler控件,是如何完成绘制的。结果,还是让我比较吃惊。
有思路。
以前虽然,我在仪表行业干过几年,编过许多类似示波器那种的实时图形,如何避免屏闪,双buffer之类的,但直到后来,我才稍稍有了点真的懂了,然后转行了。
所以,今天看了人家的实现,不免还是从心里往外佩服——虽然实现得简单直白,但总觉得很美。
所以,这里作个记录。
首先声明,还没有完全看懂。
这里就是个初稿。分析的这程,最好有两个显示器。
先来看看我们的目标:分析一下,每个Appointment的上面的那条线是如何画出来的。
首先,我们找到这个风格所在的位置
this.schedulerControl.Views.TimelineView.AppointmentDisplayOptions.StatusDisplayType = DevExpress.XtraScheduler.AppointmentStatusDisplayType.Time;
protected internal virtual ViewInfoItemCollection CreateStatusItems(AppointmentViewInfo aptViewInfo) {
switch (aptViewInfo.Options.StatusDisplayType) {
case AppointmentStatusDisplayType.Never:
default:
return new ViewInfoItemCollection();
case AppointmentStatusDisplayType.Bounds:
return CreateStatusItemsCore(aptViewInfo, aptViewInfo.Interval);
case AppointmentStatusDisplayType.Time:
TimeInterval aptInterval = aptViewInfo.AppointmentInterval;
return CreateStatusItemsCore(aptViewInfo, aptInterval);
}
}
本来想要跟一跟绘制的过程,后来,找到绘制过程的主入口:
Sources\DevExpress.XtraScheduler\DevExpress.XtraScheduler\SchedulerControl.cs
protected override void OnPaint(PaintEventArgs e) {
if (InnerIsDisposing || InnerIsDisposed)
return;
GraphicsInfoArgs info = new GraphicsInfoArgs(new GraphicsCache(e), this.Bounds);
GraphicsCache cache = info.Cache;
try {
BorderPainter borderPainter = PaintStyle.CreateBorderPainter(BorderStyle);
borderPainter.DrawObject(new BorderObjectInfoArgs(cache, ClientRectangle, null, ObjectState.Normal));
ViewPainterBase painter = CreateActiveViewPainter();
painter.DrawViewAndScrollBarSeparator(cache, this.ViewAndDateTimeScrollBarSeparatorBounds);
painter.DrawViewAndScrollBarSeparator(cache, this.ViewAndResourceNavigatorSeparatorBounds);
using (IntersectClipper clipper = new IntersectClipper(cache, ViewBounds)) {
painter.Draw(info, this.ActiveView.ViewInfo);
}
}
finally {
cache.Dispose();
}
base
在这里,有这样一句:
GraphicsInfoArgs info = new GraphicsInfoArgs(new GraphicsCache(e), this.Bounds);
GraphicsCache cache = info.Cache;
看到这时在,就很头痛。
因为,devExpress采用了缓存的机制。
这样一来,没办法在另一台显示器上,直接单步跟踪,看画到哪里了。
只好,一点点的用跳过一行的办法,来分析,跳过的那行画的是什么。
public virtual void Draw(GraphicsInfoArgs e, SchedulerViewInfoBase viewInfo) {
GraphicsCache cache = e.Cache;
DrawBorders(cache, viewInfo);
DrawCellContainers(cache, viewInfo);
DrawHeaders(cache, viewInfo);
SchedulerControl control = viewInfo.View.Control;
DrawAppointments(cache, viewInfo);
DrawMoreButtons(viewInfo, cache);
DrawNavigationButtons(cache, viewInfo.NavigationButtons, control);
}
protected virtual internal void DrawAppointments(GraphicsCache cache, SchedulerViewInfoBase viewInfo) {
AppointmentPainter.DrawAppointments(cache, viewInfo);
}
AppointmentPainter 类中的
protected internal virtual void DrawAppointments(GraphicsCache cache, SchedulerViewInfoBase viewInfo, ProcessAppointmentType selection) {
SchedulerViewCellContainerCollection containers = GetScrollContainers(viewInfo);
for (int i = 0; i < containers.Count; i++) {
SchedulerViewCellContainer container = containers[i];
AppointmentViewInfoCollection appointments = GetAppointments(viewInfo, container, selection);
if (appointments.Count > 0) {
int scrollOffset = container.CalculateScrollOffset();
DrawContainerAppointments(cache, appointments, viewInfo.View.Control, container, scrollOffset);
}
}
}
protected internal virtual void DrawContainerAppointments(GraphicsCache cache, AppointmentViewInfoCollection appointments, ISupportCustomDraw customDrawProvider, SchedulerViewCellContainer container, int scrollOffset) {
PrepareAppointmentDrawing(cache, scrollOffset);
try {
Rectangle clipBounds = CalculateContainerClipBounds(container);
clipBounds.Offset(0, scrollOffset);
using (IntersectClipper clipper = new IntersectClipper(cache, clipBounds, IntersectClipperOptions.SkipCurrentClip)) {
DrawAppointmentsCore(cache, appointments, customDrawProvider);
}
} finally {
RestoreAppointmentDrawing(cache);
}
}
protected virtual void DrawForeground(GraphicsCache cache, AppointmentViewInfo viewInfo, Rectangle viewClipBounds, ISupportCustomDraw customDrawProvider) {
DefaultDrawDelegate defaultDraw = delegate() { DrawForegroundCore(cache, viewInfo, viewClipBounds, customDrawProvider); };
if (RaiseCustomDrawAppointment(cache, viewInfo, customDrawProvider, defaultDraw))
return;
defaultDraw();
}
-----------------------------------
if (RaiseCustomDrawAppointment(cache, viewInfo, customDrawProvider, defaultDraw))
这里,激发了schedulerControl_CustomDrawAppointment事件。
给用户一次机会来处理。
-----------------------------------
protected virtual void DrawForegroundCore(GraphicsCache cache, AppointmentViewInfo viewInfo, Rectangle viewClipBounds, ISupportCustomDraw customDrawProvider) {
DrawAppointmentItems(cache, viewInfo.Items);
DrawAppointmentItems(cache, viewInfo.StatusItems);
if (viewInfo.Selected)
DrawSelection(cache, viewInfo, viewClipBounds, customDrawProvider);
}
然而,到了这里,发现不好办了:
Sources\DevExpress.XtraScheduler\DevExpress.XtraScheduler\BasePainter.cs
protected virtual void DrawAppointmentItems(GraphicsCache cache, ViewInfoItemCollection items) {
int count = items.Count;
for (int i = 0; i < count; i++)
items[i].Draw(cache, this);
}
Sources\DevExpress.XtraScheduler\DevExpress.XtraScheduler\AppointmentContentLayoutCalculator.cs
AppointmentViewInfoStatusItem类中的:
public override void Draw(GraphicsCache cache, IViewInfoItemPainter painter) {
AppointmentPainter aptPainter = (AppointmentPainter)painter;
aptPainter.DrawStatusItem(cache, this);
}
然后
protected internal override void DrawStatusItem(GraphicsCache cache, AppointmentViewInfoStatusItem item) {
Image image = item.CachedImage;
if (image != null)
cache.Graphics.DrawImage(image, item.Bounds, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel);
}
这里我们看到,代码的做法是直接往cache中贴了张图。
当然,我们自己编gid程序时,也常这么用,但这里看到,还是不免有些泄气。
还需要重头分析。
我们得找出来,什么时候,Appointment,给自己画好了图。这是我想写本文的原因。
这里的思想特别好。
目前来看,有两个思想,devExpress用到了,一个是双buffer,
另一个就是小的组件,先在内存中画好,需要的时候,把这个图直接贴上。这个主意太好了。
然后就找到这两个函数。
protected internal override void CacheStatusImage(AppointmentViewInfoStatusItem statusItem) {
Bitmap statusBitmap = CreateUnclippedStatusBitmap(statusItem);
AppointmentStatusImageCalculator calc = statusImageCalculatorFactory.GetStatusImageCalculator(statusItem.SkinElementName);
calc.MaskStatusBitmap(statusBitmap, statusItem);
statusItem.CachedImage = statusBitmap;
}
protected internal virtual Bitmap CreateUnclippedStatusBitmap(AppointmentViewInfoStatusItem statusItem) {
Rectangle bounds = statusItem.Bounds;
Bitmap result = new Bitmap(Math.Max(1, bounds.Width), Math.Max(1, bounds.Height));
using (Graphics gr = Graphics.FromImage(result)) {
gr.TranslateTransform(-bounds.X, -bounds.Y);
using (GraphicsCache cache = new GraphicsCache(gr)) {
base.DrawStatusItem(cache, statusItem);
}
}
return result;
}
protected internal virtual void DrawStatusItem(GraphicsCache cache, AppointmentViewInfoStatusItem item) {
statusPainter.DrawRectangleStatus(cache, item.BackgroundViewInfo);
if (item.ForegroundViewInfo.Interval.Duration == TimeSpan.Zero)
statusPainter.DrawTriangleStatus(cache, item.ForegroundViewInfo, item.Alignment);
else
statusPainter.DrawRectangleStatus(cache, item.ForegroundViewInfo);
}
DevExpress.XtraScheduler\DevExpress.XtraScheduler\BasePainter.cs
protected internal virtual void DrawRectangleStatus(GraphicsCache cache, AppointmentStatusViewInfo statusViewInfo) {
cache.FillRectangle(statusViewInfo.Brush, statusViewInfo.Bounds);
statusViewInfo.CalcBorderBounds(this);
DrawBorders(cache, statusViewInfo);
}
先写到这里。
虽然,找到了位置,但还是有些遗憾。
还没有想出办法,如何在调试的时修,关掉缓存。
在注掉一句话后,我们看到了预期的效果:
注掉的那句:
Sources\DevExpress.XtraScheduler\DevExpress.XtraScheduler\BasePainter.cs
代码是11.2的。
因为刚试用过最新的2014,winform这里没有本质的改动。
11.2的最大的好处是,可以用vs 2008编译过。我的电脑,虽然还可以,但vs2008还是比较快一些。
分析Devexress类库的绘制过程