I like to hang out in the microsoft.public.dotnet. framework.drawing
newsgroup. Call me a geek, an attention seeker, or just plain helpful
- I get a kick from helping others out with their drawing-related
problems, and often from just seeing how other people got together to
solve a particular problem. Questions that come up almost every day
are "How do I create a program like Visio?," "How do I draw and lay
out an interactive flowchart." and "Dudes, where can I find a sweet
process re-engineering diagramming component?"
There's a good reason for these questions. With the release
of the .NET Framework, rich-client applications have suddenly become
fashionable again. People have realized that Web pages can offer only
so much and then no more. Hooking up a rich client to a Web service
provides you with the best of both worlds - you can provide users
with a rich interface experience while still receiving the benefits
of remote connectivity and deployment.
Out of the box though, Windows Forms still doesn't cut it for
many of the types of applications developers need to write these
days, such as systems driven by flowcharts, UML diagrams, or
workflow. You've either got a lot of coding ahead of you or you need
to buy a component to do all the hard work. Writing a modeling
component from scratch is not a trivial task. A good understanding of
graphics, GDI+, and a bit of trigonometry is essential. However, a
well-written component will save you time and money and reduce the
risk to your project. Years of expertise and rich functionality are
available for download in minutes and at a tiny fraction of the cost
it would take to develop yourself.
Choosing the right component is important. The component
should allow for simple and rapid development, but be fine-grained
enough to allow the creation of specific features you want to
implement that make your system unique. The component should also be
well supported, ideally having examples in the development language
of your choice (I'm thinking VB.NET or C# here). All feature-rich
components should also have a comprehensive and accessible help file.
You may have to spend time upfront understanding the component's
object model and style of implementation, so a tutorial section and
help that's integrated directly into the IDE is always nice to have.
If you choose to develop using Windows Forms in the .NET
Framework, the component should support tight integration with the
GDI+ namespace (system.drawing) for maximum flexibility. The
component should be written in 100% managed code - you really don't
want the deployment hassles associated with any COM-based controls.
The main component classes should be inheritable to add those
features that are missing or specific to your application. A good
modeling component should support large models at least 10,000 pixels
high and wide, as most models tend to turn out much larger than the
display. For this reason your choice should also support zooming in
and scaling out of the model surface. A separate overview control
that provides a smaller, simplified version of a model so that the
user can get an overall idea of what your model or chart looks like,
as well as supporting navigation over large models, is always nice in
this situation.
Entity relationship-based models are typically made up of
shapes (nodes) and connected by lines. Top components should also
provide ways to lay out nodes depending on the type of application.
Network diagrams will probably require force-directed layouts,
whereas hierarchical models such as organization or flowcharts
require a flow type of layout. Layout is a complex and inexact
science, and you should be aware that some vendors will charge you
extra for these features, available as separate component add-ons,
and that the quality of layout may differ from vendor to vendor. A
good component should shield you from these complexities but be
flexible enough for your needs and provide ways for you to improve or
modify the layout routines yourself.
Finally, there are always some nice-to-have features. A
component that takes full advantage of GDI+ will have support for
anti-aliasing, gradients, and partial opacity that uses color or
alpha blending, making an item seem partially transparent. Using a
rich-client interface means you can take advantage of these features,
which translates into a more attractive and interesting experience
for users of your application. GDI+ doesn't rely on high-end graphics
cards such as the ones found in games machines and applications, so
components and applications driven by GDI+ can be used by any modern
PC.
Getting Down to Business
Enough talk; lets get down to some coding. Based on the above
criteria I'm going to use Crainiate's ERM .NET component to look at
some sample implementations of Windows modeling applications. Other
choices include Northwood Software's GoDiagram products or Lassalle
Technology's AddFlow component. These components can be downloaded
directly from the vendors or from sites like www.devdirect.com or
www.componentsource.com.
Although the elements they contain may be called by different
names, all models or entity relationship diagrams consist basically
of shapes (or nodes) connected by lines (or links). I'll start by
creating a simple program with two shapes and a connecting line to
show the basic principles behind creating entity relationship
diagrams.
Start by creating a new windows application with a form
called form1 that should automatically be created for you. Add the
ERM component to the toolbox if you haven't already done this and
place a Model component on form1. In the Load event handler, enter
the code in Listing 1 and run the program. You should see something
similar to Figure 1.
A blue ellipse and a red square are created and connected by
a line. The drawing2d.graphicspath class is used by the component to
draw almost any shape, which gives great flexibility. The shapes are
selectable and sizable, and can be moved at runtime as well. The line
remains docked to the nearest side of each shape it has been
connected to. Note: When one shape is dragged over another shape or
line it is partially transparent and the lines and borders of the
shapes are smoothed by anti-aliasing. This is thanks to the rich
features offered by GDI+.
Add a button to the form and enter the code in Listing 2 , run
the program, and click the button to see more interesting features
offered by GDI+, such as custom graphics paths, rotation, and regions
used by the component. It's interesting to note that the elements
created on the form aren't real controls, although they behave like
controls. Each raises their own mouse events, receives keyboard
strokes, and can contain images and text, but that's probably where
the comparison ends. Because models often contain hundreds or
thousands of items, it simply isn't desirable for a model to use a
full Windows control for each item; this would seriously degrade
performance and remove a whole lot of flexibility.
A Real-World Application
The previous example certainly looked very nice (to me
anyway) and showed the power available to be harnessed from GDI+, but
it doesn't really look like anything that could be used as an
application. In the next example we'll create a simple flowchart that
represents a credit check at a bank.
Add another form, named form2, to the project and make it the
startup object. This time place a Flowchart component on it. In the
Load event handler, enter the code in Listing 3 .
If you use Visual Basic, then add an Imports Crainiate.ERM statement
to the top of the form, or a using statement if you use C#. You
should see something similar to Figure 2.
The flowchart component class is inherited from the model
class we used in the previous example. The flowchart builds on the
basic functionality provided in the model and adds the code necessary
to draw flowchart shapes, automatically link them, and apply layout
to the flowchart. This shows one of my favorite features of the .NET
Framework - a developer can build on features from another developer
or vendor without requiring access to the source code in any way. By
applying object-oriented programming techniques to the Model class
you could quickly create your own type of chart, say a
Nassi-Shneiderman chart, without having to re-invent the wheel. This
hypothetical class could be called NSChart and would be subclassed
from the Model class, while the elements contained in the NChart -
such as processes and decisions - could be inherited from the Shape
class. This type of development is a tremendous leap forward from the
ActiveX and COM days of old and is set to become the way future
applications are developed.
Conclusion
I enjoy developing rich-client applications with Microsoft
Visual Studio .NET, and I love it when those applications include
anything that requires development with graphics components and GDI+.
By combining the .NET Framework with a well-written component you can
quickly and cost-effectively create robust, attractive, and
functional Windows modeling applications that will have your
customers and users coming back for more.
About The Author
James Westgate has created commercial solutions with Microsoft products for over 10 years. His areas of expertise range from Visual Basic, COM+, and SQL to HTML and ASP - and more recently GDI+ and C# using the .NET Framework. He has helped author many popular
interface and modeling components and enjoys real-time strategy and online first-person
shooter games. Chances are you've fragged him on many occasions.
james.westgate@crainiate.com
Listing 1
VB.NET
Dim objShape As Crainiate.ERM.Shape
'Add the first shape
objShape = Model1.Shapes.Add("rect", New Point(100, 50))
objShape.BackColor = System.Drawing.Color.White
objShape.GradientColor = Color.CornflowerBlue
objShape.GradientMode = Drawing.Drawing2D.LinearGradientMode.ForwardDiagonal
objShape.ShowGradient = True
objShape.Opacity = 80
objShape.GraphicsPath.AddEllipse(New System.Drawing.Rectangle(0, 0, 50, 50))
objShape.Insert()
'Add the second shape as a protoype
objShape = Model1.Shapes.Add("elipse", New Point(250, 50), objShape)
objShape.GradientColor = Color.Red
objShape.GraphicsPath.AddRectangle(New System.Drawing.Rectangle(0, 0, 50, 50))
objShape.Insert()
Dim objLine As Crainiate.ERM.Line
Dim objCap As New Drawing2D.AdjustableArrowCap(5, 4)
'Add a line between the two shapes
objLine = Model1.Lines.Add("line", Model1.Shapes("rect"), Model1.Shapes("elipse"))
objLine.AutoDocks = True
objLine.EndCap = Drawing.Drawing2D.LineCap.Custom
objLine.CustomEndCap = objCap
objLine.Update()
'Refresh the model
Model1.Refresh()
C#
Crainiate.ERM.Shape objShape;
//Add the first shape
objShape = model1.Shapes.Add("rect", new Point(100, 50));
objShape.BackColor = System.Drawing.Color.White;
objShape.GradientColor = Color.CornflowerBlue;
objShape.GradientMode = System.Drawing.Drawing2D.LinearGradientMode.ForwardDiagonal;
objShape.ShowGradient = true;
objShape.Opacity = 80;
objShape.GraphicsPath.AddEllipse(new System.Drawing.Rectangle(0, 0, 50, 50));
objShape.Insert();
//Add the second shape as a protoype
objShape = model1.Shapes.Add("elipse", new Point(250, 50), objShape);
objShape.GradientColor = Color.Red;
objShape.GraphicsPath.AddRectangle(new System.Drawing.Rectangle(0, 0, 50, 50));
objShape.Insert();
Crainiate.ERM.Line objLine;
System.Drawing.Drawing2D.AdjustableArrowCap objCap = new
System.Drawing.Drawing2D.AdjustableArrowCap(5, 4);
//Add a line between the two shapes
objLine = model1.Lines.Add("line", model1.Shapes["rect"], model1.Shapes["elipse"]);
objLine.AutoDocks = true;
objLine.EndCap = System.Drawing.Drawing2D.LineCap.Custom;
objLine.CustomEndCap = objCap;
objLine.Update();
//Refresh the model
model1.Refresh();
Listing 2
VB.NET
Dim objShape As Crainiate.ERM.Shape
'Suspend drawing in the model until we have added our 'new elements
Model1.SuspendModel()
'Rotate a shape
objShape = Model1.Shapes.Add("rotate", New Point(80, 160))
objShape.GradientColor = System.Drawing.Color.Orange
objShape.GradientMode = Drawing.Drawing2D.LinearGradientMode.ForwardDiagonal
objShape.ShowGradient = True
objShape.Opacity = 80
objShape.GraphicsPath.AddRectangle(New System.Drawing.Rectangle(0, 0, 50, 75))
objShape.Insert()
objShape.RotateShape(60)
'Hollow elipse
objShape = Model1.Shapes.Add("hollow", New Point(250, 160), objShape)
objShape.GradientColor = System.Drawing.Color.Lime
objShape.GraphicsPath.AddEllipse(New System.Drawing.Rectangle(0, 0, 80, 80))
objShape.GraphicsPath.AddEllipse(New System.Drawing.Rectangle(20, 20, 40, 40))
objShape.Insert()
'Add a line
Dim objLine As Crainiate.ERM.Line
objLine = Model1.Lines.Add("line2", Model1.Shapes("rotate"), Model1.Shapes("hollow"))
objLine.StartCap = Drawing.Drawing2D.LineCap.Custom
objLine.CustomStartCap = New Drawing2D.AdjustableArrowCap(5, 4)
'Make the line curved
objLine.AddPivot(1, New Point(200, 220))
objLine.Style = Crainiate.ERM.Line.LineStyleEnum.Curve
objLine.AutoDocks = True
objLine.Update()
Model1.ResumeModel()
Model1.Refresh()
Listing 3
VB.NET
Dim objPrototype As FlowShape
Dim objFlowShape As FlowShape
FlowChart1.SuspendModel()
FlowChart1.ModelSize = New Size(1200, 900)
objPrototype = FlowChart1.AddFlowShape("start", New Point(300, 50),
Crainiate.ERM.FlowShape.FlowShapeType.Connector, New Size(100, 60))
objPrototype.Text = "Start"
objPrototype.GradientColor = Color.Beige
objPrototype.Opacity = 80
objPrototype.ShowGradient = True
objPrototype.TextEdit = True
objFlowShape = FlowChart1.AddFlowShape("details", FlowChart1.Shapes("start"),
FlowShape.FlowShapeType.Decision, objPrototype, Line.LineStyleEnum.Curve)
FlowChart1.NewFlowShape.Text = "Check Details"
objFlowShape = FlowChart1.AddFlowShape("reject", FlowChart1.Shapes("details"),
FlowShape.FlowShapeType.Terminator, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Reject"
FlowChart1.NewFlowLine.Text = "Incomplete"
objFlowShape = FlowChart1.AddFlowShape("credit", FlowChart1.Shapes("details"),
FlowShape.FlowShapeType.Decision, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Check Credit"
FlowChart1.NewFlowLine.Text = "Complete"
objFlowShape = FlowChart1.AddFlowShape("deposit", FlowChart1.Shapes("credit"),
FlowShape.FlowShapeType.Decision, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Require Deposit"
FlowChart1.NewFlowLine.Text = "Fail"
objFlowShape = FlowChart1.AddFlowShape("reject2", FlowChart1.Shapes("deposit"),
FlowShape.FlowShapeType.Terminator, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Reject"
FlowChart1.NewFlowLine.Text = "Declined"
objFlowShape = FlowChart1.AddFlowShape("accept", FlowChart1.Shapes("deposit"),
FlowShape.FlowShapeType.Terminator, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Accept Conditions"
FlowChart1.NewFlowLine.Text = "Accepted"
objFlowShape = FlowChart1.AddFlowShape("reject3", FlowChart1.Shapes("accept"),
FlowShape.FlowShapeType.Terminator, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Reject"
FlowChart1.NewFlowLine.Text = "Declined"
objFlowShape = FlowChart1.AddFlowShape("approve", FlowChart1.Shapes("accept"),
FlowShape.FlowShapeType.Terminator, objPrototype, Line.LineStyleEnum.Curve)
objFlowShape.Text = "Loan Approved"
FlowChart1.NewFlowLine.Text = "Accepted"
FlowChart1.AddFlowLine("creditaccept", FlowChart1.Shapes("credit"),
FlowChart1.Shapes("accept"), Line.LineStyleEnum.Curve)
FlowChart1.NewFlowLine.Text = "Approved"
FlowChart1.ResumeModel()
FlowChart1.DoLayout()
C#
FlowShape objPrototype;
FlowShape objFlowShape;
FlowChart1.SuspendModel();
FlowChart1.ModelSize = new Size(1200, 900);
objPrototype = FlowChart1.AddFlowShape("start", new Point(300, 50),
Crainiate.ERM.FlowShape.FlowShapeType.Connector, new Size(100, 60));
objPrototype.Text = "Start";
objPrototype.GradientColor = Color.Beige;
objPrototype.Opacity = 80;
objPrototype.ShowGradient = true;
objPrototype.TextEdit = true;
objFlowShape = FlowChart1.AddFlowShape("details",(FlowShape)
FlowChart1.Shapes["start"],Crainiate.ERM.FlowShape.FlowShapeType.Decision, objPrototype,
Crainiate.ERM.Line.LineStyleEnum.Curve);
FlowChart1.NewFlowShape.Text = "Check Details";
objFlowShape = FlowChart1.AddFlowShape("reject", (FlowShape)
FlowChart1.Shapes["details"], FlowShape.FlowShapeType.Terminator, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Reject";
FlowChart1.NewFlowLine.Text = "Incomplete";
objFlowShape = FlowChart1.AddFlowShape("credit", (FlowShape)
FlowChart1.Shapes["details"], FlowShape.FlowShapeType.Decision, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Check Credit";
FlowChart1.NewFlowLine.Text = "Complete";
objFlowShape = FlowChart1.AddFlowShape("deposit", (FlowShape)
FlowChart1.Shapes["credit"], FlowShape.FlowShapeType.Decision, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Require Deposit";
FlowChart1.NewFlowLine.Text = "Fail";
objFlowShape = FlowChart1.AddFlowShape("reject2", (FlowShape)
FlowChart1.Shapes["deposit"], FlowShape.FlowShapeType.Terminator, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Reject";
FlowChart1.NewFlowLine.Text = "Declined";
objFlowShape = FlowChart1.AddFlowShape("accept", (FlowShape)
FlowChart1.Shapes["deposit"], FlowShape.FlowShapeType.Terminator, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Accept Conditions";
FlowChart1.NewFlowLine.Text = "Accepted";
objFlowShape = FlowChart1.AddFlowShape("reject3", (FlowShape)
FlowChart1.Shapes["accept"], FlowShape.FlowShapeType.Terminator, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Reject";
FlowChart1.NewFlowLine.Text = "Declined";
objFlowShape = FlowChart1.AddFlowShape("approve", (FlowShape)
FlowChart1.Shapes["accept"], FlowShape.FlowShapeType.Terminator, objPrototype,
Line.LineStyleEnum.Curve);
objFlowShape.Text = "Loan Approved";
FlowChart1.NewFlowLine.Text = "Accepted";
FlowChart1.AddFlowLine("creditaccept", (FlowShape) FlowChart1.Shapes["credit"],
(FlowShape) FlowChart1.Shapes["accept"], Line.LineStyleEnum.Curve);
FlowChart1.NewFlowLine.Text = "Approved";
FlowChart1.ResumeModel();
FlowChart1.DoLayout();
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com