|
|
|
Select your preferred language |
Using DirectX in Windows Forms
controls.
Recently, Microsoft released DirectX9
which has a new set of managed wrappers that enable you to use DirectX through
C# or VB.NET applications.
Many articles exist on how to create a
game under DirectX but the speed and power of DirectX is not just for games.
Some user interfaces can require graphics speed that just isn't available from
GDI+ without acceleration.
This article examines the use of DirectX
as a valuable addition to the arsenal of the user interface programmer. A
prerequisite for this article is the DirectX9 SDK which can be found on the
Microsoft site as a free download.
The subject chosen for this article is a
scrolling marquee control that uses DirectX to create a fast, smooth scrolling
marquee which doesn't load the processor unduly. It uses the 2 dimensional
DirectDraw API's.
The DirectDraw system uses some highly
optimised code that takes full advantage of all hardware acceleration afforded
by the system. Generally, a DirectDraw application uses at least a double buffered
approach to drawing, creating an image in one or more passes in memory and
finally copying that image to the screen. These in-memory or on-screen images
that can be manipulated are called Surfaces. In full screen mode, the most
commonly used for DirectDraw applications, the visible image, or primary
surface, covers all of the screen. However, in an application such as a form or
control
DirectDraw is used in windowed mode and only a portion of the screen is
affected.
The DXMarquee control uses three direct draw surfaces. The
primary surface on which the final output is diplayed, a back-buffer surface
used to assemble the image and an off-screen image that contains the text drawn
with GDI+ in the font and colour required.
Figure 1 shows the relationship between
the application and the various drawing surfaces.

Figure 1: DirectX drawing surfaces and their relation to each
other
Although the primary surface covers the whole screen, only
the portion covered by the window is updated. The update can be done
infrequently but of course, DirectX means speed so often a
timer is set to update the window between 30 and 50 times a second.
The AppWizard provided for DirectX doesn't provide an option
for creating a Windows Forms control. It's also true to say that the full-screen
and windowed applications that are created by the wizard are unnecessarily
complex so examining them closely for clues on how to do it will result in a
control that's built like a battleship. The points to note are; Create a directX
Device, set the cooperative level to windowed mode, create the primary surface
and any secondary surfaces you may need then get on with the job of drawing the
control.
The Marquee control takes a simple line of text and scrolls
it across the surface of the control at speeds both slow and blisteringly fast.
To do this, rather than drawing the marquee image at each draw cycle, the image
is only recreated when the text changes, The appearance of moving the text is
managed by blitting the static image of the text to different positions in the
back-buffer and then copying the back-buffer to the screen. The basic control is
unremarkable inasmuch as it's created by the new
project wizard as a class library. We don't even need to bother with a control
library because UserControl has baggage we don't want.
An important difference to note between DirectX blitting and
GDI+ is that the source and destination rectangles must be wholly inside their
respective surfaces. You cannot attempt to blit a 150 pixel wide segment from a
surface that is only 100 pixels wide. If you try this, an exception will be
thrown so it's important to ensure that all source and destination rectangles
are carefully constructed.
The DXMarquee control
Starting from a simple class library, the DXMarquee control
was first given references to the System.Drawing DLL for the text capabilities,
the System.Windows.Forms DLL for the control base class, the Microsoft.DirectX
and Microsoft.DirectX.DirectDraw DLL's for the DirectX features.
Initializing the control
First the DirectX device is created and the cooperative level
set to Normal. This enables Windowed mode. Then the timer is initialized so that
updates will be fired
public DXMarquee()
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.UserPaint, true);
//Create
the device and set the cooperative level.
_device=new Device();
_device.SetCooperativeLevel(this,CooperativeLevelFlags.Normal);
t.Tick+=new
EventHandler(t_Tick);
t.Interval=Interval;
t.Enabled=true;
}
Public
Sub New()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint
Or ControlStyles.UserPaint,
True)
'Create
the device and set the cooperative level.
_device
= New Device
_device.SetCooperativeLevel(Me,
CooperativeLevelFlags.Normal)
AddHandler
t.Tick, AddressOf t_Tick
t.Interval
= Interval
t.Enabled
= True
End
Sub
Creating the DirectX surfaces.
The three DirectX
surfaces, _primary, _backBuffer and _textBuffer are created. _primary covers the
whole screen but we only use a portion, _backBuffer is always the same size as
the control client area and _textBuffer is as large as the measured string
containing the text taking into account the font size. The primary surface is
provided with a clipper object that clips output to the area defined by the
control surface. This method is called whenever the control size changes or the
text is modified because the back-buffer and the off-screen image buffer will
probably be different sizes too.
protected
void CreateSurfaces(Bitmap bm)
{
SurfaceDescription
sd=new SurfaceDescription();
//Create
DirectX surfaces. First the primary surface.
if(_primary==null)
{
sd.SurfaceCaps.PrimarySurface=true;
_primary=new Surface(sd,_device);
}
//This
can be called if the control changes size so we always create a new backbuffer
if(_backBuffer!=null)
_backBuffer.Dispose();
//now
the back buffer surface as an offscreen plain buffer built from the bitmap
handed in
sd.Clear();
sd.Width=this.Width;
sd.Height=this.Height;
sd.SurfaceCaps.OffScreenPlain=true;
_backBuffer=new
Surface(sd,_device);
if(_textBuffer!=null)
_textBuffer.Dispose();
//now
the image surface from which the text is copied
sd.Clear();
sd.SurfaceCaps.OffScreenPlain=true;
sd.Height=bm.Height;
sd.Width=bm.Width;
_textBuffer=new
Surface(bm,sd,_device);
//Finally,
the clipper is set up to ensure the output is limited to the area of this
control
if(_primaryClipper!=null)
_primaryClipper.Dispose();
_primaryClipper=new
Clipper(_device);
_primaryClipper.Window=this;
_primary.Clipper=_primaryClipper;
}
Protected
Sub CreateSurfaces(ByVal
bm As Bitmap)
Dim
sd As New
SurfaceDescription
'Create
DirectX surfaces. First the primary surface.
If
_primary Is Nothing
Then
sd.SurfaceCaps.PrimarySurface
= True
_primary = New Surface(sd, _device)
End
If
'this
can be called if the control changes size so we always create a new backbuffer
If
Not _backBuffer Is
Nothing Then
_backBuffer.Dispose()
End
If
'now
the back buffer surface as an offscreen plain buffer built from the bitmap
handed in
sd.Clear()
sd.Width
= Me.Width
sd.Height
= Me.Height
sd.SurfaceCaps.OffScreenPlain
= True
_backBuffer
= New Surface(sd, _device)
If
Not _textBuffer Is
Nothing Then
_textBuffer.Dispose()
End
If
'now
the image surface from which the text is copied
sd.Clear()
sd.SurfaceCaps.OffScreenPlain
= True
sd.Height
= bm.Height
sd.Width
= bm.Width
_textBuffer
= New Surface(bm, sd, _device)
'Finally,
the clipper is set up to ensure the output is limited to the area of me control
If
Not _primaryClipper Is
Nothing Then
_primaryClipper.Dispose()
End
If
_primaryClipper
= New Clipper(_device)
_primaryClipper.Window
= Me
_primary.Clipper
= _primaryClipper
End
Sub
Creating the text.
As the control properties such as Font, ForeColor, BackColor and Size change,
so too does the appearance of the control. Each time one of these property
change events is raised, the CreateText method makes a new offscreen surface
containing all the text in the right font and colour.
protected void
CreateText()
{
if(this.Height==0
|| this.Width==0)
return;
Font=new
Font(Font.FontFamily,0.6f*this.Height);
Graphics g=CreateGraphics();
g.TextRenderingHint=TextRenderingHint.AntiAlias;
SizeF
sf=g.MeasureString(this.Text,this.Font,2048,StringFormat.GenericTypographic);
_textWidth=(int)(0.5f+sf.Width);
if(_textWidth==0)
return;
if(_bm!=null)
_bm.Dispose();
_bm=new
Bitmap(_textWidth,this.Height);
g=Graphics.FromImage(_bm);
g.Clear(this.BackColor);
g.TextRenderingHint=TextRenderingHint.AntiAlias;
SolidBrush
sb=new SolidBrush(this.ForeColor);
g.DrawString(Text,Font,sb,0,0,StringFormat.GenericTypographic);
sb.Dispose();
_marqueeOffset=0;
this.CreateSurfaces(_bm);
}
Protected
Sub CreateText()
If
Me.Height = 0 Or
Me.Width = 0 Then
Return
End
If
Font =
New Font(Font.FontFamily, 0.6F *
Me.Height)
Dim
g As Graphics = CreateGraphics()
g.TextRenderingHint
= TextRenderingHint.AntiAlias
Dim
sf As SizeF = g.MeasureString(Me.Text,
Me.Font, 2048, StringFormat.GenericTypographic)
_textWidth
= CInt(0.5F + sf.Width)
If
_textWidth = 0 Then
Return
End
If
If
Not _bm Is
Nothing Then
_bm.Dispose()
End
If
_bm =
New Bitmap(_textWidth,
Me.Height)
g =
Graphics.FromImage(_bm)
g.Clear(Me.BackColor)
g.TextRenderingHint
= TextRenderingHint.AntiAlias
Dim
sb As New
SolidBrush(Me.ForeColor)
g.DrawString(Text,
Font, sb, 0, 0, StringFormat.GenericTypographic)
sb.Dispose()
_marqueeOffset
= 0
Me.CreateSurfaces(_bm)
End
Sub
The CreateText
method also resets the offset of the marquee from the right edge of the control.
Timer tick handler
The timer drives the motion of the display. It updates the offset of the text
and invalidates the display
private void
t_Tick(object sender, EventArgs e)
{
_marqueeOffset+=Step;
if(_marqueeOffset>this.Width+_textWidth)
_marqueeOffset=0;
}
Private
Sub t_Tick(ByVal
sender As Object,
ByVal e As
EventArgs)
_marqueeOffset
+= Nstep
If
_marqueeOffset > Me.Width + _textWidth
Then
_marqueeOffset
= 0
End
If
Invalidate()
End
Sub
Painting the control
The paint routine does not use the Graphics provided at-all. It drives the
assembly of the text into the correct position in the back-buffer along with the
placement of the background colour and then copies the assembled back-buffer to
the primary surface.
protected override
void OnPaint(PaintEventArgs e)
{
//the
back bufffer is copied to the primary surface.
if(_primary==null
|| this._backBuffer==null)
return;
//define
the destination rectangle
Rectangle dest=new Rectangle(this.ClientSize.Width-_marqueeOffset,0,this._textWidth,this.Height);
dest.Intersect(new
Rectangle(0,0,this.Width,this.Height));
//The
source is a moving window onto the pre-drawn bitmap.
//The
marquee starts from the right and scrolls left
//so
the offset is used to calculate the amount of text seen
Rectangle src=new
Rectangle(0,0,Math.Min(dest.Width,_textWidth),this.Height);
if(_marqueeOffset>this.Width)
src.Offset(this._marqueeOffset-this.Width,0);
src.Intersect(new
Rectangle(0,0,_textWidth,this.Height));
_backBuffer.ColorFill(this.BackColor);
if(src.Width!=0
&& dest.Width!=0)
{
_backBuffer.Draw(dest,_textBuffer,src,DrawFlags.DoNotWait);
_primary.Draw(RectangleToScreen(this.ClientRectangle),
_backBuffer, this.ClientRectangle,
DrawFlags.DoNotWait);
}
base.OnPaint(e);
}
Protected
Overrides Sub
OnPaint(ByVal e As
System.Windows.Forms.PaintEventArgs)
'the
back bufffer is copied to the primary surface.
If
_primary Is Nothing
Or Me._backBuffer
Is Nothing
Then
Return
End
If
'define
the destination rectangle
Dim
dest As New
Rectangle(Me.ClientSize.Width - _marqueeOffset,
0, Me._textWidth, Me.Height)
dest.Intersect(New
Rectangle(0, 0, Me.Width,
Me.Height))
'The
source is a moving window onto the pre-drawn bitmap.
'The
marquee starts from the right and scrolls left
'so
the offset is used to calculate the amount of text seen
Dim
src As New
Rectangle(0, 0, Math.Min(dest.Width, _textWidth), Me.Height)
If
_marqueeOffset > Me.Width
Then
src.Offset(Me._marqueeOffset
- Me.Width, 0)
End
If
src.Intersect(New
Rectangle(0, 0, _textWidth, Me.Height))
_backBuffer.ColorFill(Me.BackColor)
If
(src.Width <> 0 And dest.Width <> 0)
Then
_backBuffer.Draw(dest,
_textBuffer, src, DrawFlags.DoNotWait)
_primary.Draw(RectangleToScreen(Me.ClientRectangle),
_backBuffer, Me.ClientRectangle,
DrawFlags.DoNotWait)
End
If
End
Sub
Miscellaneous notes.
There are properties Interval and Step that enable you to modify the speed of
the display and each of the important On<propertychanged> methods are overridden
to re-create the text if the size, font or colours change.
The code for the complete control is listed here
Summary
Once built, the DXMarquee control was dragged onto a form where it shows-off
it's scrolling even in design time. The scrolling is quick and smooth but most
importantly, in comparison to an equivalent control created using GDI+ and
standard GDI+ blitting techniques, the processor usage is miniscule. On my
machine a Dell 650mHz laptop, the processor loading was in the region of a
percent or two. As a test I created a form with ten DXMarquee controls all
running at a good speed and they all ran happily and smoothly with only a 10-12%
CPU usage. This was in contrast to the GDI+ version which loaded the CPU 100%
and several of the controls ground to a halt entirely.
DirectX can be successfully used in a Windows Forms control to enhance the
user interface experience greatly by providing fast, smooth graphics.
Return to the main index.