GDI+ Programming-Creating Custom Controls Using C#

Eric White

With

Chris Garrett
Simon Robinson

Copyright © 2002 by Apress

All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

The authors and publisher have made every effort in the preparation of this book to ensure the accuracy of the information. However, the information contained in this book is sold without warranty, either express or implied. Neither the authors, nor Apress, nor its dealers or distributors will be held liable for any damages caused or alleged to be caused either directly or indirectly by this book.

ISBN: 1861006314

Trademark Acknowledgements

Apress has endeavored to provide trademark information about all the companies and products mentioned in this book by the appropriate use of capitals. However, Apress cannot guarantee the accuracy of this information.

Credits

  • Authors
    Eric White
    Chris Garrett
    Simon Robinson

  • Technical Reviewers
    Neil Avent
    Cristian Darie
    Mitch Denny
    David Schultz
    Morgan Skinner

  • Commissioning Editor
    Ian Blackham

  • Technical Editors
    Mankee Cheng
    Tony Davis
    Dev Lunsford
    Ian Nutt

  • Managing Editor
    Louay Fatoohi

  • Project Manager
    Claire Robinson

  • Author Agent
    Cilmara Lion

  • Production Coordinator
    Sarah Hall

  • Cover
    Natalie O'Donnell

  • Indexer
    Martin Brooks

  • Proofreader
    Chris Smith

Dedication

This book is dedicated to my father, David White.
Thanks for the Bugbooks, Dad!

About the Authors

Eric White

Eric White is an independent software consultant with over 20 years experience in building management information systems, accounting systems, and other types of rich-client and n-tier database applications. He has written custom controls in numerous windowing systems, including all versions of Microsoft Windows since 2.0, Macintosh, OS/2, X/Motif, and others. Eric has particular interest in object-oriented design methodologies, including use case analysis, UML, and design patterns. After years of working with too many varieties of technologies to list, he is currently specializing in C#, Visual Basic .NET, ASP.NET, ADO.NET, XML, COM+, GDI+, SQL Server, and other Microsoft technologies.

Chris Garrett

Chris Garrett is the Technical Manager for one of the larger European new media agencies, working with some of the world's best-known brands helping them develop web and interactive TV strategies.

Chris started his obsession with programming when, at around the age of 10, he started learning BASIC on his Commodore VIC-20. He never managed to kick this addiction, and since 1994 he has focused on the Internet, most recently using ASP.NET.

When Chris isn't hunched over a computer screen or talking someone's ear off about some new technology, he tries very hard to have a real life with his wife Clare, his daughter Amy, and his two overweight cats.

Simon Robinson

Simon Robinson lives in Lancaster, UK, where he shares a house with some students. He first encountered serious programming when he was doing his PhD in physics, modeling all sorts of weird things to do with superconductors and quantum mechanics. The experience of programming was nearly enough to put him off computers for life (though, oddly, he seems to have survived all the quantum mechanics), and for a while he tried being a sports massage therapist instead. But he then realized how much more money was in computers compared to sports massage, and rapidly got a job as a C++ programmer and researcher instead. Simon is clearly the charitable, deep, spiritual type, who understands the true meaning of life.

Programming eventually led him into writing, and he now makes a living mostly from writing great books for programmers. His work has taken him through many different aspects of Windows programming and he routinely writes code in C++, C#, and Visual Basic. He is very enthusiastic about the .NET Framework, which he firmly believes is set to revolutionize programming. His spare time is spent either at dance classes (he loves performing arts and is studying the subject in his spare time) or on his pet project, writing a computer strategy game.

You can visit Simon's web site at http://www.SimonRobinson.com.

Next Section


Table of Contents

GDI+ Programming: Creating Custom Controls Using C#
GDI+ Programming: Creating Custom Controls Using C#
byEric White, Chris GarrettandSimon Robinson ISBN:1861006314
Apress 2002 (536 pages)

This book provides a comprehensive guide to the use of GDI+ in .NET applications and aims to provide developers with all the information they need to build effective custom controls.

Table of Contents
 | GDI+ Programming—Creating Custom Controls Using C#
 | Introduction
 | Chapter 1 | - | An Overview of GDI+ and Custom Controls
 | Chapter 2 | - | Drawing Surfaces
 | Chapter 3 | - | Pens and Brushes
 | Chapter 4 | - | Text and Fonts
 | Chapter 5 | - | Images
 | Chapter 6 | - | GraphicsPaths and Regions
 | Chapter 7 | - | Clipping and Invalidation
 | Chapter 8 | - | Transformations
 | Chapter 9 | - | Printing
 | Chapter 10 | - | An Alternative Coordinate System
 | Chapter 11 | - | Architecture and Design of Windows Forms Custom Controls
 | Chapter 12 | - | Design-Time Support
 | Chapter 13 | - | Scrolling
 | Chapter 14 | - | Mouse Events and Cursors
 | Chapter 15 | - | GDI+ Images in ASP.NET
 | Chapter 16 | - | Creating Custom ASP.NET Server Controls
 | Chapter 17 | - | Web Services and GDI+
 | Index
 | Back Cover


Introduction

The release of the .NET Framework has changed the programming landscape for those seeking to develop applications for Microsoft-based customers. New object-oriented languages such as C# and Visual Basic .NET have been created specifically for this new platform, and so much of the knowledge acquired in previous environments needs to be migrated and new opportunities need to be identified.

The area of graphics programming has been affected as much as any other. Whereas the Windows Graphical Device Interface (GDI) was once used to facilitate custom drawing to a device, the .NET Framework now gives us GDI+. GDI+ consists of a number of namespaces and classes, made available as part of the .NET Framework, and designed specifically to allow us to carry out custom drawing in our web and Windows controls. Effectively acting as a wrapper around the old GDI, GDI+ provides an easy-to-understand, inheritance-based object model for use by .NET programmers.

The sheer usability of GDI+ may draw many more programmers into custom drawing. Those from a C++/MFC background will already be familiar with many of the approaches in GDI+; meanwhile, those who worked with Visual Basic will find in GDI+ an accessible way into developing bespoke controls that augment those shipped with the framework.

The part of the .NET Framework base-class library that falls under the GDI+ banner is substantial, and in a book of this size we can't hope to cover every angle. Therefore, the approach we take in this book is to concentrate on the areas and features of GDI+ that are relevant to programmers who are most likely to work with these classes-that is, programmers looking to develop custom controls. We will be covering the creation of Windows Forms custom controls in this book in great depth, but we will also touch on creating custom ASP.NET server controls during the final three chapters.

Who is this Book For?

This book is designed to appeal to any developer who wishes to appreciate and exploit the functionality of the GDI+ base classes provided as part of the .NET Framework. In particular, the book is aimed at those working in a business-application environment, presenting techniques and information suitable to those aiming to accomplish the task of writing GDI+ code for Windows Forms and Web Forms in order to create custom controls.

The examples in this book (both the GDI+ demonstration examples in the early part of the book, and the application of GDI+ within custom controls later on) are presented in the C# language. They should be accessible both to intermediate-level C# programmers and to any relatively experienced developer capable of recognizing the standard constructs and features of an object-oriented language.

However, the choice of C# for the examples in this book is less important than the fact that it is a book about developing with GDI+ and custom controls in .NET. Indeed, the GDI+ techniques covered here are language-neutral-they can be applied, with the appropriate language knowledge, using other .NET Framework languages such as Visual Basic .NET and Visual C++ .NET.

Previous Section Next Section


What Does this Book Cover?

Structurally, this book falls naturally into four sections. The first section consists of a single chapter, , which provides a brief overview of GDI+, its purpose and structure, and gives some context for the development and use of custom controls in .NET applications. At the end of that chapter, a short example illustrates the salient features of the Visual Studio .NET environment that we will consistently use throughout the book.

The second section of the book consists of Chapters 10, and may loosely be considered to tackle the nuts and bolts of GDI+ programming. We'll look at all of the classes, methods, properties, and events that are most important to the application of GDI+ in custom controls, and we'll include plenty of description and demonstration:

  • In we'll look at GDI+ fundamentals such as drawing surfaces and coordinate systems. It includes an introduction to the Graphics object, which encapsulates a drawing surface. We'll see that there are basically three types of drawing surface (screens, bitmaps, and printers). Our Graphics object provides us with methods for drawing and painting onto our drawing surface, as well as for managing many other features-for example, measuring units, transformations, clipping, and invalidation. Hence, the Graphics class provides the foundation for much of the rest of the book.

  • In we'll cover pens and brushes. GDI+ makes a distinction between drawing and filling, and this fact is encapsulated well in the existence and juxtaposition of the Pen and Brush classes. These two classes are fundamental graphics tools, and we'll try to capture their versatility in this chapter.

  • In

  • moves on to discuss images and image manipulation, and even how to create and save images.

  • Chapters move on to the important subject of paths and regions-encapsulated in GDI+ by the GraphicsPath and Region classes. First, we see how these classes further highlight the distinction between drawing outlines and filling areas; they enable us to work with more irregular shapes than previously considered, and to group sets of shapes together for more effective processing. Then, we build on this knowledge by explaining how these objects can be used in operations such as clipping and invalidation, to create some useful and powerful effects.

  • explains how to handle transformations in GDI+. While translations, rotations, and shearing aren't commonly used in custom control development, this chapter will highlight some interesting techniques.

  • Having focused on screen-based rendering in these early chapters, discusses the subject of printing. We develop an appreciation of how to control differences between what we see on the screen and what the printer outputs to the page.

  • describes how to set up an alternative coordinate system, which you may find helpful in your development of custom controls. It involves subtle changes in the semantics of the Graphics class. We can't inherit from the Graphics class itself (because it is defined as sealed), but we can 'modify' its behavior by employing a design pattern called the decorator pattern-in which we define our own class whose methods have identical signatures to the methods in the Graphics class.

From here onwards, the chapters begin to focus more specifically on custom control architecture and development. The third section of the book introduces the concept of a custom control and covers the issues we need to consider when deciding what features to build into our application, as well as taking into account the requirements of the user interface:

  • First things first, we have a review of custom controls in , including a definition, characteristics exhibited by them, and comparisons with components. We will consider some of the custom control theory at a high level before moving on to examples of custom controls, demonstrating their behavior, such as properties, focus, events, derivation from custom controls, and composite custom controls.

  • In , we consider some of the ways in which we can build in functionality for our custom control to make it easier for other developers to use our controls in their code. More specifically, we will focus on better integrating our controls with Visual Studio .NET, such as creating modal boxes and drop-down boxes to edit properties. All these features are to make life easier, at design time, for the developer using our control.

  • When building a custom control, we'll often require a virtual space that is greater than the physical space. We encounter this problem in

  • In , we will look at another user interaction feature: the mouse. We'll see how to develop our control so that it performs different functions when the mouse is used in different ways, including hit testing, mouse event routing, dragging, and drag-scrolling. By their nature, graphical user interfaces are visual, so we'll also take a look at cursors in our applications, and what we can do with them.

In the final section of the book, we move the focus of using GDI+ functionality from Windows Forms to Web-based situations. The last three chapters of the book will consider situations in which the developer needs to use the power of ASP.NET to serve GDI+ images, create custom controls across the Web, and utilize web services technology. We do not cover these situations in great depth, but we do provide a framework for using GDI+ in these different situations:

  • In , we will look at serving GDI+ images across the Internet. First, we will grab an image from a web server's file system and serve it to the client application view, an ASP.NET page, using GDI+ code. We will look at other web image manipulation techniques, such as mixing HTML and dynamic images, modifying images with GDI+, and finally, the creation of pie charts.

  • In , we will extend our understanding of custom controls by creating some custom ASP.NET server controls. These are similar in nature to Windows Forms custom controls, but are implemented very differently. We will see examples of ASP.NET user controls in this chapter, including a button user control, and an example that creates and manipulates thumbnails, and we'll discuss support for design-time features.

  • In the final chapter of the book, , we shall look at how GDI+ can be used in web services for returning images and image data. We begin with a brief discussion of the thinking behind web services, and then we will see three increasingly complex examples of web services that use GDI+. We will examine the simple Hello World service, then move on to a web service that returns an image from a URL, and finally, a web service that returns data from an image to draw a pie chart.


What You Need to Use this Book

To run the samples in this book, you will need:

  • A suitable operating system. At the time of writing, Microsoft recommends Windows 2000 (Professional, Server or Advanced Server edition) with Service Pack 2, or Windows XP Professional edition.

  • The .NET Framework SDK.

The examples in this book are developed using the Visual Studio .NET IDE. It is possible to build the examples without the VS.NET IDE, but we don't cover that in the book.

We recommend that you also download the complete source code for the examples in this book, from http://www.wrox.com (see the Customer Support and Feedback section below). Whether you plan to cut-and-paste the code from these source files, or type the code for yourself, the source code provides a valuable way to check for errors in your code.


Style Conventions

We have used certain layout and font styles in this book that are designed to help you to differentiate between the different kinds of information. Here are examples of the styles that are used, with an explanation of what they mean.

As you'd expect, we present code in two different ways: in-line code and displayed code. When we need to mention keywords and other coding specifics within the text (for example, in discussion relating to an if…else construct or the GDI+ Graphics class) we use the single-width font shown in this sentence. If we want to show a more substantial block of code, then we display it like this:

private void Form1_Paint(object sender,
                          System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
}

Sometimes, you will see code in a mixture of gray and white backgrounds, like this:

private void Form1_Paint(object sender,
                          System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    g.DrawRectangle(Pens.Black, 10, 10, 50, 40);
}

In cases like this, we use the gray shading to draw attention to a particular section of the code-perhaps because it is new code, or because it is particularly important to this part of the discussion. We also use this style to show output that is displayed in the console window.

Advice, hints, and background information comes in an indented, italicized font like this.

Important

Important pieces of information (that you really shouldn't ignore) come in boxes like this!

Bulleted lists appear indented, with each new bullet marked as follows:

  • Important Words are in a bold type font.

  • Words that appear on the screen, or in menus like File or Window, are in a similar font to the one you would see on a Windows desktop.

  • Keys that you press on the keyboard like Ctrl and Enter are in italics.


Customer Support and Feedback

We value feedback from our readers, and we want to know what you think about this book: what you liked, what you didn't like, and what you think we can do better next time. You can send us your comments, either by returning the reply card in the back of the book, or by e-mail to <feedback@wrox.com>. Please be sure to mention the book's ISBN and title in your message.

Source Code and Updates

As you work through the examples in this book, you may choose either to type in all the code by hand, or to use the source code that accompanies the book. Many readers prefer the former, because it's a good way to get familiar with the coding techniques that are being used.

Whether you want to type the code in or not, it's useful to have a copy of the source code handy. If you like to type in the code, you can use our source code to check the results you should be getting-it should be your first stop if you think you might have typed in an error. By contrast, if you don't like typing, then you'll definitely need to download the source code from our web site! Either way, the source code will help you with updates and debugging.

Therefore all the source code used in this book is available for download at http://www.wrox.com. Once you've logged on to the web site, simply locate the title (either through our Search facility or by using one of the title lists). Then click on the Download Code link on the book's detail page and you can obtain all the source code.

The files that are available for download from our site have been archived using WinZip. When you have saved the archive to a folder on your hard drive, you need to extract the files using a decompression program such as WinZip or PKUnzip. When you extract the files, the code will be extracted into chapter folders. When you start the extraction process, ensure that your decompression program is set to use folder names.

Errata

We have made every effort to make sure that there are no errors in the text or in the code. However, no one is perfect and mistakes do occur. If you find an error in this book, like a spelling mistake or a faulty piece of code, we would be very grateful to hear about it. By sending in errata, you may save another reader hours of frustration, and of course, you will be helping us provide even higher quality information. Simply e-mail the information to <support@wrox.com>; we'll check the information, and (if appropriate) we'll post a message to the errata pages, and use it in subsequent editions of the book.

To find errata on the web site, log on to http://www.wrox.com/, and simply locate the title through our Search facility or title list. Then, on the book details page, click on the Book Errata link. On this page you will be able to view all the errata that have been submitted and checked through by editorial. You will also be able to click the Submit Errata link to notify us of any errata that you may have found.

Technical Support

If you would like to make a direct query about a problem in the book, you need to e-mail <support@wrox.com>. A typical e-mail should include the following things:

  • In the Subject field, tell us the book name, the last four digits of the ISBN (6314 for this book), and the page number of the problem.

  • In the body of the message, tell us your name, contact information, and the problem.

We won't send you junk mail. We need these details to save your time and ours. When you send an e-mail message, it will go through the following chain of support:

  1. Customer Support- Your message is delivered to one of our customer support staff- they're the first people to read it. They have files on most frequently asked questions and will answer anything general about the book or the web site immediately.

  2. Editorial- Deeper queries are forwarded to the technical editor responsible for the book. They have experience with the programming language or particular product, and are able to answer detailed technical questions on the subject. Once an issue has been resolved, the editor can post the errata to the web site.

  3. The Authors- Finally, in the unlikely event that the editor cannot answer your problem, they will forward the request to the author. We do try to protect the author from any distractions to their writing; however, we are quite happy to forward specific requests to them. All Wrox authors help with the support on their books. They will mail the customer and the editor with their response, and again all readers should benefit.

Important

Note that the Wrox support process can only offer support to issues that are directly pertinent to the content of our published title. Support for questions that fall outside the scope of normal book support is provided via the community lists of our http://p2p.wrox.com/forum.

p2p.wrox.com

For author and peer discussion join, the P2P mailing lists. Our unique system provides programmer to programmer™ contact on mailing lists, forums, and newsgroups, all in addition to our one-to-one e-mail support system. Be confident that your query is being examined by the many Wrox authors, and other industry experts, who are present on our mailing lists. At p2p.wrox.com you will find a number of different lists that will help you, not only while you read this book, but also as you develop your own applications.

To subscribe to a mailing list just follow this these steps:

  1. Go to http://p2p.wrox.com/ and choose the appropriate category from the left menu bar.

  2. Click on the mailing list you wish to join.

  3. Follow the instructions to subscribe and fill in your e-mail address and password.

  4. Reply to the confirmation e-mail you receive.

  5. Use the subscription manager to join more lists and set your mail preferences.


Chapter 1: An Overview of GDI+ and Custom Controls

Overview

Writing graphics code is one of the most enjoyable tasks in the computer programming arena. We may be building a custom graphical window that presents data in a more visible, more accessible fashion. We may be interactively creating graphics that are served to the user as part of a web site. Whatever we're doing in this area, the job of writing code to create colorful, effective graphics is an immensely pleasing and satisfying one.

Elsewhere, in the custom control we have one of the most powerful approaches to application development. A well-designed custom control adds to our toolbox a tool that we can reuse time and time again. With a number of such controls providing large parts of our application's functionality, building the application can simply be a case of adding each control to the application and setting its properties.

Thus, when we bring these two application areas together we have quite an animal. When we employ graphics code in our custom controls we have a very enjoyable way to dramatically improve programmer productivity, while simultaneously increasing the usability and visual appeal of our applications. By building a variety of custom controls, we can affect the entire appearance and usability of our application. We can add custom controls that deftly fit in with the standard Windows controls, or we can supplant the look and feel of the entire set of Windows controls with our own, and thus create a game-like environment that is appealing to an entirely different audience.

This book is going to focus on this combination, and specifically on the creation of custom controls using graphics in the .NET Framework using the C# language. In effect this means we'll be digging into GDI+ -the part of the .NET Framework base-class library for graphics programming. We'll begin by looking at the GDI+ basics, before moving on to their application in the creation of useful controls.

In this opening chapter we're going to lay the foundations for later discussions by looking at three main areas:

  • What GDI+ is-its relationship to the .NET Framework, its purpose, how it relates to previous graphics libraries, and a brief introduction to the namespaces and classes it encompasses

  • What custom controls are-an overview of Windows Forms and Web Forms custom controls, and how .NET helps us to create them

  • A first example (developed using the Visual Studio .NET IDE) of programming with GDI+

So, let's start by examining GDI+ and the .NET Framework.


GDI+ and .NET

Most readers of this book will already be aware of the many and varied reasons for adopting .NET and will already be familiar with C#. But as a starting point, let's just highlight two benefits of the .NET platform that are particularly relevant to this book:

  • Rich client application development via Windows Forms. Windows Forms is a very cleanly designed class hierarchy for building rich client applications. A highly interactive environment with properties and events, combined with a truly object-oriented approach, makes for an exceptionally powerful toolset. When used with the distributed computing features in the .NET Framework, we can build n-tier applications easily. The facilities for building custom controls are the best that I have ever seen.

  • Web application development via ASP.NET and Web Forms. Together, ASP.NET and Web Forms provide a wonderful environment for building web sites. Many of the ideas and technologies in ASP.NET were previously evident in other environments, but there are also some completely new ideas. The architecture of ASP.NET is such that we avoid the debugging issues that are problematic with other similar technologies, and among all the integrated development environments I've seen, Visual Studio .NET gives the best features for improving programmer productivity.

In both of these areas, we have an advanced set of features to help us create custom controls such as:

  • True integration of properties. Properties are implemented in a Visual Basic-like style.

  • Effective event handling. The inclusion of delegates as an integral part of the language provides a very good mechanism for writing event handlers.

  • A better design-time environment. Visual Studio .NET gives us an unparalleled level of power for building custom controls. Not only can we seamlessly deploy custom controls in the Design window, but we can also explicitly control the appearance and manipulation of the controls and implement property editors in a variety of ways. The inclusion of attributes in the languages allows us to configure the design time environment.

  • A powerful graphics device interface. As we'll see in this book, using GDI+ to build custom controls gives us a broad and colorful palette with which we can ‘paint' our applications.

  • Effective infrastructure for distribution of custom controls. After we've built our custom controls, we may want to distribute them to developers in other programming groups, or sell them. We may also want to purchase commercial custom controls from others. The assembly infrastructure in .NET solves the problems of previous component distribution technologies.

We've seen some of these features in other development platforms before, but until now, we've never seen them all in a single platform. Their combination provides a powerful infrastructure for building custom controls.

What is GDI+?

GDI+ is Microsoft's new .NET Framework class library for graphical programming. Because it is part of the .NET Framework, it is, of course, object-oriented. Organized into six namespaces, it is a set of classes that are designed to work together. In this way is it similar to other areas of functionality in .NET, such as ADO.NET. Let's have a look at some of the major features of GDI+:

  • GDI+ provides us with three types of drawing surfaces-windows, bitmaps, and printers.

  • GDI+ provides us with tools that allow us to draw two-dimensional 'line-drawings' on any drawing surface. This includes drawing lines, many types of shapes and polygons, and curves, with an infinite variety of brushes and pens. We have a number of types of transformations at our disposal, allowing us to create sophisticated effects with great ease.

  • The text drawing features in GDI+ are extensive (and the anti-aliasing technology is particularly impressive).

  • GDI+ has image and bitmap support-we can read images and draw them onto any drawing surface. We can also create images and draw into them.

  • GDI+ supports printing-print preview capabilities are obtained easily, with very little additional effort on the part of the developer.

  • GDI+ is designed to work with any kind of .NET application.

In this book, we're going to focus on the use of GDI+ in Windows Forms and Web Forms. Since GDI+ is part of the .NET Framework, and hence available to both Windows Forms and ASP.NET applications, we can (via an appropriate class design) write our custom graphical drawing code in such a way that we can use the same code in both types of application. If we use the .NET Framework to write web services, we can wrap the same graphical code for use in those applications, and hence (for example) serve up custom images to clients of the web services that we write.

So, when building ASP.NET applications, GDI+ can be used to create images and serve them to the client's browser. When implementing a server farm, we can load balance by distributing a computationally intensive operation that creates graphical images over a number of computers or processors. Client applications often need to display graphics, either in custom controls, or in forms. Web services can also make use of GDI+ to distribute graphical image generation between computers separated by a firewall, or servers using technologies other than .NET.

Before we get too carried away, we should place GDI+ in context and briefly consider the underlying technology-the Windows Graphical Device Interface (GDI).

GDI and GDI+

GDI+ is based on GDI, which is the part of the Windows API that handles graphics. In fact, GDI+ is effectively a 'wrapper' around GDI (similar to the way that MFC wrapped GDI with its own classes). GDI offers a layer of abstraction, shielding the application developer from the need to program for each specific display device that the application may come across. Thus, when you want your application to draw to a screen, the application executes the appropriate GDI function and then GDI works out how to communicate that to the video card. GDI is typically implemented through C or C++ programs (although many languages, including Visual Basic, C#, VB.NET, etc. can use GDI directly in much the same way that they are able to use many other parts of the Windows API).

While GDI+ sits above GDI, it offers two significant advantages:

  • First, GDI+ adds capabilities to GDI by the inclusion of classes that implement functionality that would be difficult to write in straight GDI. To use such functionality in a GDI program, we would need to either acquire a library that supplies that functionality, or write it ourselves. (For example, GDI+ gives us specific enhancements for drawing semi-transparently, access to gradient brushes, support for a wider range of image formats, easier programming of graphics paths and regions, and more powerful transformations. We'll encounter all of these during the course of this book.)

  • Second, GDI+ is fully integrated into the .NET Framework-GDI is a fairly complex, inaccessible technology, whereas GDI+ has made the job of the programmer much easier.

The integration of GDI+ into the .NET Framework and its improved programming interface combine to bring other benefits:

  • GDI+ uses an object-oriented class hierarchy. Programming GDI+ consists of creating objects, setting their properties, and calling their methods.

  • GDI+ uses method overloading. Thus, when we call a method of a GDI+ object, we often have the opportunity to choose the version (or overload) of the method that takes the set of arguments most appropriate for the situation.

  • There is no intrinsic reason why we should draw the outline of a shape and fill its interior in the same operation. However, GDI combines the two operations, and that leads to programmatic idiosyncrasies such as hollow brushes and hollow pens. GDI+ recognizes the distinction between drawing outlines and filling regions, and provides two distinct operations for these tasks. The result is a far more flexible drawing interface.

  • GDI+ hides some aspects of state management from the developer. With GDI, we get a device context for a drawing surface, and then pass the device context as an argument to the functions that do the drawing. This device context maintains quite a bit of state, such as the current pen, current brush, and current point (when drawing lines etc.). GDI+ shields the programmer from this complexity. This facilitates a simpler approach that eliminates the bugs that can occur when a developer doesn't completely set all necessary state (and thus ‘inherits' state randomly from other parts of the code).

    For example, every line drawing method in GDI+ requires a Pen object. GDI+ knows what pen is currently part of the state of GDI, and if the pen passed as an argument to the GDI+ method is different from the pen currently selected in the device context, then GDI+ automatically changes the pen in the device context. If the pen passed to the method is the same as that currently selected in the device context, then GDI+ doesn't need to change the pen in the device context.

    GDI+ still maintains some state, such as transformations and clipping regions, but these can arguably be considered to be an attribute of the drawing surface, and should be maintained as state, whereas the notion of a 'current pen' really is quite artificial.

Two Class Libraries, both Called GDI+

When you start using GDI+ in Visual Studio .NET, you will (probably very soon) come across the fact that, in addition to the library called GDI, there are also two programming libraries that both bear the name GDI+. This might seem rather alarming, but the explanation is simple.

One of the GDI+ programming libraries is the .NET GDI+ library-the set of namespaces and classes that was released as part of the .NET Framework and forms the subject of this book. The other one has its roots in the Microsoft Foundation Classes (MFC)-the MFC includes a number of C++ classes that encapsulate GDI, and it just happens that this class library is also named GDI+.

The MFC GDI+ is very similar to the managed GDI+ that comes with the .NET Framework, and which we use with C# in this book. The MFC GDI+ contains classes whose names are very similar to those in the .NET GDI+, and whose functionality is also comparable. However, the two libraries are different.

When searching within the .NET documentation, you will notice both MFC GDI+ data types and .NET GDI+ data types. With a little care and know-how, it's very easy to distinguish the two. For instance, if you try a search for information on the DrawRectangle method (by searching on the string DrawRectangle, without filtering the search), you'll get results that include both. It lists the .NET GDI+ DrawRectangle method by using the ‘dot' notation, and noting that it's a method:

Graphics.DrawRectangle Method

By contrast, it lists the MFC GDI+ DrawRectangle function by using C++ notation for its signature (and involving the use of pointers):

Graphics::DrawRectangle(Pen* pen, RectF& rect)

If that's not sufficient, the Location column in the Search Results pane also tells us whether a particular class or method is part of the .NET GDI+ (signified by .NET Framework Class Library) or the MFC GDI+ (signified by the phrase GDI+: Platform SDK):

Click To expand

This book only covers the managed code (.NET) GDI+. We don't explore the MFC GDI+ at all Beyond this point in the book, we won't refer to the MFC GDI+ again.

So, to be explicitly clear, when we mention GDI we mean the graphics programming library that is part of the Windows API, and when we mention GDI+ we're referring to the managed code GDI+ class library that comes as part of the .NET Framework.

GDI+ benefits from the perspective gained from the experience of building many graphical programming interfaces. It is a very clean, consistent, and powerful set of high-performance classes that provide most of the graphics functionality that we need. However, GDI+ is new. There are places where its functionality is limited. Where GDI+ doesn't meet our needs, we can always drop down into the GDI API, and use GDI in conjunction with GDI+. We'll take a look at how to do this later in the book when we discuss how to handle scrolling and mouse events in custom controls.

DirectX and GDI+

DirectX is another Microsoft technology related to graphics programming. However, DirectX differs significantly from GDI+ in both purpose and programming interface. DirectX is a suite of multimedia APIs that give applications the ability to access features of PC hardware (such as sound and graphics acceleration cards), enabling them to deliver high performance output. Such high-quality presentation is commonly encountered in PC games such as Doom, where 3D effects are a crucial part of the application.

So, while GDI+ and DirectX are both libraries for writing graphical applications, in our consideration of graphics for business-type rich-client and web applications, we only need to concern ourselves with GDI+.

The GDI+ Namespaces

As we've already said, all the GDI+ functionality is contained in six namespaces that are included in the .NET Framework. Those six namespaces are described as follows:

Namespace | Description
System.Drawing | Provides basic graphics functionality, including drawing surfaces, images, colors, brushes, pens, and fonts.
System.Drawing.Drawing2D | Provides advanced raster and vector graphics functionality.
System.Drawing.Imaging | Provides advanced imaging functionality, over-and-above that which is provided in the System.Drawing namespace.
System.Drawing.Printing | Provides print and print preview functionality.
System.Drawing.Text | Provides advanced font functionality, over-and-above that which is provided in the System.Drawing namespace.
System.Drawing.Design | Provides functionality for enhancing design-time support of custom controls. Includes classes for developing custom UITypeEditor classes, which allow us to customize behavior of custom controls in the Design window of Visual Studio .NET.

All of these namespaces are contained in a DLL file called System.Drawing.dll. We'll meet many of the classes themselves as we progress through the early chapters of this book; indeed, we'll begin to make use of them in the first example later in this chapter.

Since the focus of the book is the use of GDI+ in the construction of custom controls, our next task is to review the problem domain we're looking to address.


Custom Controls

There are two fundamental types of custom control for use in .NET:

  • Windows Forms custom controls are used in rich client applications. These are typically highly responsive custom controls-they may track the mouse when it enters the control, and they may change the mouse cursor or even the appearance of the custom control.

  • Web Forms custom controls are used in Web applications. These controls will typically be less dynamically interactive.

Within both Windows Forms and Web Forms custom controls, there are a variety of types of control, each requiring a different programming approach, and each used for a different purpose. In this book, we'll cover more than just the varieties of custom controls that use GDI+-in both Windows Forms and Web Forms, we can create custom controls by combining several existing controls to yield useful time-saving components. Similarly, we don't have to build a custom control to use GDI+ with either Windows Forms or Web Forms, but later, we'll see that it is often better to build a custom control than simply to draw in a Form or Panel.

Let's overview the mechanics and functionality of custom controls by considering:

  • Windows Forms custom controls

  • Web Forms custom controls

  • The creation of generic code for both types of custom control

Building Windows Forms Custom Controls

There are extensive facilities in the .NET Framework for building Windows Forms custom controls. The framework defines how we should build our custom control, and if we follow the conventions, our custom control need not be just a standalone control in a window-it can be used as a sibling to other Windows Forms controls.

When we build custom controls in Windows Forms, we derive from the Control class, the UserControl class, or any existing control or custom control. Then, we can write code that responds to Paint events, draws whatever graphics we want, and renders them. Using GDI+, perhaps in conjunction with other controls, we can thus create the 'look' of our custom control.

Inside our control we may build code to respond to keyboard and mouse events. By following certain conventions, our custom control can participate in the user's navigation between other controls-both native controls and other custom controls. Our keyboard and mouse event handlers-perhaps (again) in conjunction with other controls-combine to create the 'feel' of our custom control. We can add properties, methods, and events to our custom control as we develop it.

Visual Studio .NET Support for Custom Controls

Again, by following certain conventions, Visual Studio can host our control at design time. At the most basic level this means we'll be able to:

  • Modify the layout of our custom control in the Design window

  • Edit the properties and events (both the events that we defined, and those that the control inherited from its base class) of our custom control in the Properties window

  • Add our custom control to the Toolbox window for dragging and dropping into the Design window as appropriate

We can enhance our custom control so that it participates even more fully in the Visual Studio .NET IDE. There are two separate 'modes' that the custom control needs to operate in. The most obvious mode is the one in which we run our custom control in the application. In this mode, the custom control performs the functions that we designed it for. More interestingly, custom controls can also function in a design-time mode (seen when the control is viewed in the Design window).

As an example, consider an elaborate multi-column grid control. Run in the application, we'll see the data with which we populate the grid. However, run in the Design window, we might not see the actual data, but instead see the name of the database table and column that supplies the data. In the application, we can click on cells and change the data. In the Design window, we can right click on the cells, and change the source of the data.

This represents a significant benefit to developers. Often, in other environments, the specialized code that renders the custom control in the Design window mode was embedded in the IDE itself. This meant that if we developed new, elaborate custom controls, we didn't have the capability to change the IDE so that our new custom control could allow the developer to manipulate the setup of the control. The IDE typically had some default behavior for modifying the setup of the control. We typically needed to set properties to configure the custom control. We could not modify the design-time implementation of the control to allow for such things as right-clicking to change the data source. This meant that when we developed new, elaborate custom controls, in the IDE we often only saw a rectangle that represented the position of the custom control.

However, in Visual Studio .NET all such design-time rendering and interaction is now separate from the implementation of the IDE. We can add code to our custom control that enhances the design-time support, and we can write additional classes that completely customize the appearance and developer interaction of the custom control within the design-time environment. In fact, the ‘factory-delivered' controls that come with the .NET Framework use exactly the same mechanism that is available to us when building our own custom controls.

Web Forms Custom Controls

A great deal of web application structure involves server-side controls that generate HTML. The client is usually a web browser, and expects to receive only HTML (and perhaps some client-side scripting code). However, ASP.NET and GDI+ provide us with a unique alternative approach to building custom controls for web pages.

The facilities for building custom controls in ASP.NET are very similar to those for Windows Forms. Again we can build controls with properties and events. The main difference is that the task of rendering the control consists of the generation of HTML, client-side script, and images that are then sent to the client's browser.

There are two approaches that we can take when building custom controls such as these:

  • We can generate HTML and client-side script. This approach allows us to build custom abstractions that simplify generation of HTML. For example, if we were building a web site for a newspaper, we could build a custom control that encapsulates a ‘story'. We could set properties for the headline, for the story, for the lead photo, and perhaps even for an accompanying advertisement. Then, our custom control would do all of the specific HTML generation for the story.

  • We can generate an image and subsequently process mouse events for the image. A lot of the capabilities of GDI+ can be used with this type of ASP.NET custom control. This approach is very similar to the approach that we can take with Windows Forms.

The second approach is the one in which we have the highest integration of GDI+ with an ASP.NET custom control, and can use GDI+ to the best effect. Therefore, this is the approach that we will focus on in the later chapters of this book.

Using Image-Generating Custom Controls

In ASP.NET, we have the capability to generate an image for a page dynamically at the time that the HTML for the page is being generated. This means that we can generate images at run time, based on state stored in our custom control, or state stored in the various other state mechanisms within ASP.NET. In addition, when the image is rendered on the user's screen, the user can click on the image and the mouse click can be processed with an event handler that changes state as appropriate, or regenerates the image when the page is posted again to the user. In effect, we can build custom controls that the user can manipulate by clicking on them.

There are a couple of performance considerations associated with this type of control:

  • This design requires a roundtrip from the client to the server each time that the user clicks on the control, so this type of control would be most appropriate where the user would only desire to click a limited number of times in a typical interaction. It would not generally be appropriate to use this type of control to implement a hierarchical control (in which the user might want to expand and contract a large number of nodes). It would, however, be an ideal type of control if we wanted to allow the user to click on a bar in a bar chart to get more information.

  • Each time the user clicks and thus requests a refreshed page, the server must generate a new image and transmit it back to the client. Therefore, this technique is less suitable for any custom control with large dimensions; but if the control is small or can be highly compressed then the performance issue is minimized.

Of course, there are exceptions to these general rules. For example, this kind of control would be ideal for a mapping application that displays each map in the form of an image, and allows the user to use mouse clicks to zoom in on a particular portion of the displayed map. This is true even though the image may be large, and even though the user might want to click many times. There is no other feasible way to implement this type of web application.

Because each user event requires a roundtrip from the client to the server, a custom control that allows the user to drag with the mouse is not appropriate to implement using this technique. If this is a requirement of our application, and the application must be a web application, as opposed to a rich client application, we should use some type of mobile code. The .NET Framework gives us the capability to execute code on the client in a similar fashion to Java applets; this technology is called 'Windows Forms Controls in the Browser' or browser controls. For more information on this technology, please refer to Professional Windows Forms (Wrox, ISBN 1-86100-554-7).

Creating Generic Code for both Windows Forms and Web Forms

Of course, the programming models for these two types of custom control are quite different:

  • When writing a Windows Forms control, we must derive from the System.Windows.Forms.Control or System.Windows.Forms.UserControl class, and paint in a window. We handle mouse and keyboard events directly.

  • When writing a Web Forms control, we must derive from the System.Web.UI.Control class, paint to an Image object, and serve the image object to the browser. To handle mouse events (which are, of course, initiated on the client), the event must be transmitted to the server, then sent to our custom control by the ASP.NET infrastructure, and a new page generated and sent to the client.

Notwithstanding these differences, it's still true that both types of controls can use GDI+. Consequently, it is possible to create drawing code that is sufficiently generic that it can be incorporated into both types of controls, and even reused for drawing to a printed page. To do this, we write drawing code that is parameterized, perhaps taking parameters such as the size of the drawing surface, the color depth of the drawing surface, and the like. Our drawing code can then draw correctly on drawing surfaces of varying resolutions.

To make a Windows Forms custom control, we call this parameterized code to get the drawn image, and then we draw it into our custom control. In a similar fashion, we can wrap drawing code to make a Web Forms custom control. To complete this chapter, we'll walk through a brief example that demonstrates a few lines of GDI+ code in a Windows Forms custom control.


A First Example

In this example, as in all the examples in the first part of the book, we'll use the Paint event handler directly to demonstrate the features of GDI+ that we discuss. In this first example only, we'll step through screenshots of the Visual Studio .NET IDE that we'll see as we put the example together. In the remaining chapters, we'll just show you the code and leave you to work out the IDE details for yourself-the assumption being that most (but not all) readers of this book will already by quite familiar with the Visual Studio .NET IDE.

Similarly, as we expect most readers to have a familiarity with C#, we won't be discussing the theory behind delegates, events, and event handlers here. More details on those topics can be found in books like Professional C# 2nd Edition (Wrox, ISBN 1-86100-704-3).

So, in this brief review of the basics we'll be looking at:

  • Creating new projects and working with event handlers for the Load and Paint events

  • Working with namespaces and assemblies

  • Writing output to the console window

  • Writing code that releases resources appropriately via use of the Dispose method

Creating a New Project

Start Visual Studio .NET. To begin with, we are going to create a new Visual C# Windows application. On the menu, select File | New | Project. This opens the New Project dialog box:

Click To expand

We're going to build a C# Windows application here. Therefore, in the Project Types pane, select Visual C# Projects; and in the Templates pane, select Windows Application.

Elsewhere in the book we'll also use Class Library projects, Windows Control Library projects, and ASP.NET Web Application projects. But this example (like all the examples in the first part of the booh) uses this simple C# Windows Application format to demonstrate the GDI+ code discussed.

Type a name for your new application, and select the place on your hard disk where you want VS.NET to store the project files. Then click OK, and VS.NET will set up your project.

By default, this process results in an application that contains a blank form named Form1. In your IDE, you should see that Visual Studio automatically opens the Design window for this form:

Click To expand

We'll often use Form1 to demonstrate GDI+ code samples-in many of the chapters dealing with detailed aspects of GDI+ it's not the nature of the form that's important to the example, but the GDI+ code. For this reason, we don't even bother renaming the form-we'll just leave it as Form1.

Creating an Event Handler for the Load Event

We'll make use of a couple of different events in our control-the Load event and the Paint event. Let's concentrate on the Load event first. When the control is loaded into memory, the Load event fires-and causes the Load event handler to execute (if there is a Load event handler).

So, let's create a Load event handler, so that we'll be able to see this happen. As it happens, the Load event is the so-called 'default event' for the Form control. One consequence of this is that there is an especially easy way to create an event handler for this particular event. Just double-click on the Form1 form (shown in the Design window in the screenshot above). When you do this, the IDE creates an empty event handler for the default event, and the code that 'wires up' the event handler to the control. It also switches to the code window (labeled Form1.cs), which contains the code for the control:

Click To expand

Since the IDE has taken us to the code view, let's use it to take a look at the extra code it's just generated. The screenshot above shows the empty Load event handler that was generated. We'll return to the event handler itself in just a moment; but let's take a moment to see the other code that the IDE just generated here. To find it, click on the+symbol adjacent to the label Windows Form Designer generated code-this will expose all the code in this code region. It includes an InitializeComponent routine of the form:

private void InitializeComponent()
{
   this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
   this.ClientSize = new System.Drawing.Size(300, 300);
   this.Name = "Form1";
   this.Text = "Form1";
   this.Load += new System.EventHandler(this.Form1_Load);
}

The shaded line above adds an instance of a delegate created from our event handler to the Load event. It's this code that enables the framework to call our event handler at the appropriate time.

Now modify the event handler, as follows:

private void Form1_Load(object sender, System.EventArgs e)
{
    MessageBox.Show("The control is in the process of loading...");
}

To see this working, you can compile and run the application (by selecting Debug | Start or Debug | Start Without Debugging in the IDE, or by using the corresponding shortcuts F5 or Ctrl-F5). You should see a message box appear, bearing this message:

Click To expand

When you click OK, it will disappear and the following window will appear:

Click To expand
Creating an Event Handler for the Paint Event

All this is fine if we want to create an event handler for the Load event, but what about other event handlers? Well, we'll use the Paint event a lot in this book, so let's set about creating a Paint event handler.

In the IDE, click on the Form1.cs [Design] window so that you can see the empty Form1 form again. Single-click on the form itself, so that it has the focus-when you do this, you should be able to see the Form1 properties in the Properties pane (probably at the bottom left of your IDE window-if it's not there, press F4 to reveal it and bring it into focus). Click on the Events button (the lightning bolt) in the Properties pane, so that it displays the list of possible events for the form. We can see that the Load event has been assigned an event handler called Form1_Load:

Click To expand

To create an event handler for the Paint event, find the event in the Properties window. Then you can either:

  • Double-click on the event in this view of the Properties window, to create an event handler with the default name Form1_Paint.

  • Type a name for your Paint event handler into the right-hand column, opposite the Paint event listed in the left-hand column, and hit Return. This will create an event handler with the name that you type.

Whichever you choose, this action once again causes the IDE to generate some new code for our form, and switches the main view back to the code window (Form1.cs). This time, the IDE generates code for an empty event handler (this time for the Paint event):

private void Form1_Paint(object sender,
                          System.Windows.Forms.PaintEventArgs e)
}
{

It also generates the code that 'wires up' the event handler to the control, in the InitializeComponent routine:

this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);

Back in the Form1 Properties window (which you can see by returning to the Design view and hitting F4), we can now see that the Paint event has been assigned an event handler called Form1_Paint:

Click To expand

Now let's modify the Paint event handler so that it paints something.

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
   Graphics g = e.Graphics;
   g.FillRectangle(Brushes.White, this.ClientRectangle);
   g.DrawString("The control has painted this text string onto the form...",
                this.Font, Brushes.Black, 0, 0);
}

We've modified the Paint event handler to fill the form's client rectangle with white, then draw the string in black, in the default font for the form, at location (0, 0).

Press F5 or Ctrl-F5 to build and run the application. After we dismiss the message box that announces the Load event, we'll see the message painted in the form.

Click To expand
Removing an Event Handler

Occasionally, it will be useful to disable an event handler. To do this, we simply need to remove the 'wiring' code (in the InitializeComponent routine) that connects the event handler to the control. You can do this manually, or (perhaps preferably) you can use the IDE. To use the IDE, return to the Design view and press F4 to bring up the Properties pane. Then find the event in the list of events, and delete the name of the corresponding event handler that appears in the right-hand column:

Click To expand

In fact, this has two effects. First, it removes the 'wiring' code from the InitializeComponent routine. Second, if the event handler is empty, it will delete the event handler code, in an attempt to keep the code as tidy as possible. Note that if you've written any custom code into the event handler, then this action does not delete the event handler-so you won't accidentally lose code this way.

Many of the examples in this book are intended to focus tightly on GDI+ aspects-so to keep those examples simple in every other respect, we paste the GDI+ code directly into a Paint event handler.

Coping with Namespaces and Assemblies

As we've already seen in this chapter, the classes that make up the GDI+ feature set are contained in six namespaces that are provided by the .NET Framework. When you're writing controls or testing examples that use the classes in these namespaces (or, indeed, in any of the .NET Framework's other namespaces), then you need to state explicitly in your control's code that you intend to use those namespaces. If you don't, then you'll get an error message like this when you try to compile the project:

The type or namespace name ‘Drawing' does not exist in the class or namespace ‘System' (are you missing an assembly reference?)

Let's take a look at a quick example that does that, and what we do to fix it. We'll use the C# Windows application that we created earlier in this section. Make sure that you have a Paint event handler (use the directions above to create one, if necessary), and modify it so that it reads as follows:

private void Form1_Paint(object sender,
                        System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    GraphicsPath gp = new GraphicsPath();

    gp.AddLine(10, 10, 50, 10);
    gp.AddLine(50, 10, 50, 50);

    g.DrawPath(Pens.Black, gp);
}

Now attempt to compile the application. The attempted compilation should fail-in fact, you should get four errors in the Task List window:

Click To expand

In fact, these four errors all point to the same problem, which is that we're trying to use an instance of the GraphicsPath class, which is part of a .NET Framework namespace called System.Drawing.Drawing2D. However, the application is not able to use anything that belongs to that namespace, because we have not explicitly stated that it should use the contents of that namespace.

You can find the code that lists the included namespaces, near the top of the code for the control:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

Each of these lines is a using directive, and each line specifies a different namespace that we would like the application to use. As you can see, there are six namespaces listed here, but System.Drawing.Drawing2D is not among them. So, to solve this problem, we simply need to add another using directive, like this:

using System.Drawing.Drawing2D;

Now try again. The code should compile this time, and when it runs you should see a simple image with a couple of lines:

Click To expand

Over the course of the book, we use all six namespaces. Where we do, we've attempted to indicate the namespace involved so that you can insert the correct using directives into your application. There's more information on the contents of each namespace in the MSDN online notation-try http://msdn.microsoft.com/library/en-us/cpref/html/cpref_start.asp as a starting point.

Adding a Reference to an Assembly, to Make a Namespace Available

It's reasonable to ask where the System.Drawing.Drawing2D namespace comes from. Are all namespaces available to us in the same way? The answer is that they are available, if you make them available.

As we said earlier, the System.Drawing.Drawing2D namespace is contained in an assembly called System.Drawing.dll. The namespace is available to us because the application contains a reference to the assembly.

To see this, take a look at the Solution Explorer pane in your IDE window, and take a look under the References node. You'll see a number of references, one of which is System.Drawing. If you look at the properties of this reference (by highlighting System.Drawing and pressing F4 to bring up the Properties window), you'll see that this is a reference to the file System.Drawing.dll:

Click To expand

It's this reference that makes the System.Drawing.Drawing2D namespace available to us through a simple using directive in our code.

In fact, the Visual Studio .NET IDE includes a reference to the System.Drawing.dll library file by default, whenever it creates a Windows Application (because it expects that the application is going to involve some amount of drawing activity). If, for example, you created a Console Application instead, you'd find that there's no reference to System.Drawing.dll.

Adding a reference to a type library is easy-in the Solution Explorer pane, simply right-click on the References node, and select Add Reference. This will bring up the Add Reference dialog, and you just use it to navigate to the file or project you need:

Click To expand

Using the Console in a Windows Application

It is often handy-even in a Windows application-to be able to write output to the console window. This is particularly useful, for example, in situations where you're debugging an application and you want to get extra information about what's happening under the covers.

You could output information to message boxes, or to some other GUI, but there are circumstances where this really isn't an option. Using message boxes for this type of information is particularly cumbersome and intrusive. More importantly, when you're tracing a bug that is related to the order of events, using another message box or GUI will affect the order of events. Indeed, the debugger itself can certainly alter the order of events. In these cases, sending output to the console window is a very useful alternative.

Note, the Debug and Trace classes that we get with the .NET Framework have a lot more functionality than the Console class we use here. However, to keep this aspect of the code in this book as simple as possible, we use the Console class.

To write text output to the console window, we use the Console class. The Console class is part of the System namespace, so the first thing you need to do is check that that the System.dll library is referenced in your application:

Click To expand

Then you should check that you've got a using directive for this namespace:

using System;

With this in place, using the Console class in a Windows application is easy. There are two main approaches, and we'll see both here. First, modify the code in the Load event handler of your Windows application, so that it writes some text to the console upon the Load event:

private void Form1_Load(object sender, System.EventArgs e)
{
    Console.WriteLine("This text is written to the console " +
                      "when the form is loaded...");
}

Now let's apply this code in two different ways, and see the results.

Using the Output Window in Visual Studio

If we run the application in the Visual Studio .NET IDE, with debugging enabled, then the text we write to the console shows up in the Output Window:

Click To expand

If you don't see this in your IDE, select View | Other Windows | Output on the menu (or press Ctrl+Alt+0).

Using the Command Prompt Window

Alternatively, we can force the output to appear in a console window. To do this, we have to modify the application so that it is no longer a Windows application, but a Console application. To do this, go to the Solution Explorer pane, right-click on the application name, and select Properties from the context menu:

Click To expand

This opens the Properties pages for the project. Now, you'll recall that when we first created this application, we created it as a Windows Application-and hence, the value specified for the Output Type property is Windows Application. Change it to Console Application, and click OK.

Now, we can run the application in any way that we choose-in the IDE with debugging, or from the IDE without debugging, or even by running the application as an EXE from outside the IDE. When we run the application, we'll see a command prompt window (in addition to the graphical windows and controls)-and everything that we write using the Console class appears in that window:

Click To expand

Classes that Implement IDisposable

There are a certain number of classes in the .NET Framework that use some Windows resources. The term resource is used in a very particular way, in the context of Windows programming. The term refers to a special kind of memory that was used for certain types of operation, and in the early days of Windows programming, this special memory was very limited. As soon as our computer ran out of resources, strange things would start to happen: the Windows OS would begin to run very slowly, and applications would not be drawn correctly.

In those days, it was incumbent upon any application to free resources as soon as possible after it had finished using them. With the introduction of the Windows 9x operating systems (including Windows ME), Microsoft increased the amount of resource memory available to applications-but they did not make it unlimited. Windows 2000 (according to its documentation) does not have any finite limit on resources; however, I have seen Windows 2000 misbehave when too many applications are open, and I've sometimes been able to restore correct behavior by closing some applications.

In the .NET Framework, many classes implement the IDisposable interface, which includes the Dispose method. When we're using an instance of one of these classes, it is highly desirable to call its Dispose method as soon as we are done with it-because it's at this point that the object releases its Windows resources.

The Dispose method is called in the destructor of any such object-but that does not remove the need to call Dispose explicitly. The reason for this is related to the way that destruction occurs in managed languages. In unmanaged C++, when the object is deleted, the destructor is called immediately. However, in C# and other managed languages, it's the responsibility of the garbage collector to call the destructor. The definition of the behavior of the garbage collector is such that we can't be certain when it will run. In particular, if we have machines with a large amount of memory (which many do these days), the garbage collector may run very infrequently. This is not a big problem on Windows 2000 and XP OSs; but if our application is running on Windows 9x, our application very well may run out of resources before the computer runs out of memory. Running out of memory triggers the garbage collector to run, but running out of resources does not.

The .NET Framework runs on the Windows 9x operating systems, so if we are planning on distributing our application on that series of operating systems, it is important to call Dispose whenever we are done with these objects. Of course, it may be less important if we are not targeting those operating systems; but it is still good programming practice to release our resources when we are done with them (even on Windows 2000).

Knowing When to Call the Dispose Method

Consider the following GDI+ code, contained in a Paint event handler:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Pen p = new Pen(Color.Aquamarine, 5);
    g.DrawLine(p, 10, 10, 50, 50);
    p.Dispose();
}

This code uses a Graphics object and a Pen object-both of which are instances of GDI+ classes that we'll meet formally in the coming chapters. The issue here is that both the Graphics class and the Pen class implement the IDisposable interface (you can confirm this by checking the documentation)-but this code calls the Dispose method only on the Pen object.

In fact, this is intentional and correct. The reason is that the Graphics object was created by the framework before it sent the Paint event: it is a property of the PaintEventArgs object e that is passed into the event handler. Therefore, we did not create the Graphics object itself within our event handler-so we shouldn't dispose of it.

By contrast, the Pen object is our own creation, and so it's correct for us to dispose of it when we've finished with it.

Important

The rule is: if our method created it, then our method should dispose of it.
The Dispose Method and the using Block

If we're applying a C# using block within our code, and we create objects within the using block, then the situation is slightly different. Namely, we're still responsible for disposing of objects that we create, but we don't need to call the Dispose method explicitly because C# does it at the end of the using block (when the object goes out of scope). The following code demonstrates this:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    using (Pen p = new Pen(Color.Aquamarine, 5))
    {
        g.DrawLine(p, 10, 10, 50, 50);
    }
}

According to the .NET documentation, the above code is precisely equivalent to the following:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Pen p = new Pen(Color.Aquamarine, 5);
    try {
        g.DrawLine(p, 10, 10, 50, 50);
    }
    finally {
        if (p != null)
            ((IDisposable)p).Dispose();
    }
}

We can create multiple objects in the using block, separating them by commas; however, they need to be of the same type:

using (Pen p  = new Pen(Color.Aquamarine, 5),
           p2 = new Pen(Color.Red))

If you're unfamiliar with the C# language structure, you should note the two distinct uses for the using keyword in the language. The first is in the form of a using block (which delimits scope, as shown here). The second is the using directive (which allows us to use the types of a namespace without spelling out the full namespace name of the type each time we use it).

The only possible complaint that we might have with the using block is that it creates another level of indentation, but the benefits are threefold:

  • We know that the object can't be disposed of before its time

  • We can be sure that the resource is released, even if an exception is raised

  • The using block avoids the possibility that the resource has been disposed of twice, by making sure that the reference is not null


Summary

In this chapter, we took an overview of GDI+, considered how custom controls can be used in the .NET Framework, and stepped through a basic example of programming GDI+ with C# to set ourselves up for the rest of the book.

As part of our scene setting we saw that GDI+ is based on GDI-it is a layer over GDI, but adds a bit of functionality to GDI. So, while we'll usually take advantage of the new, improved programming interface for graphical operations that GDI+ offers, we can always use GDI directly when GDI+ doesn't meet our needs. Furthermore we introduced the six namespaces that make up GDI+ and identified some of the classes and data types that will be most commonly used throughout the book.

Moving on to the area of custom control design and implementation in .NET we identified three key features that make custom control technology powerful: the ability to produce controls with defined properties, defined events, and a customizable design-time appearance.

Finally, in this chapter, we covered four areas that you should be familiar with before proceeding with the rest of the book. These four areas are:

  • Managing event handlers in Visual Studio

  • Configuring our projects to include various assemblies and namespaces

  • Using the Console class

  • Correctly using classes that implement IDisposable

In this book, with regards to GDI+, we are going to do three different things. First, we are going to present the basic semantics of GDI+ (not all of them, only the most important 80% of them). Second, we are going to clarify areas of the GDI+ semantics that are not explained well in the documentation. Finally, we are going to present tips, techniques, and strategies that will help with writing graphical code and custom controls.


Chapter 2: Drawing Surfaces

Overview

When writing GDI+ code, there are three basic target environments for the resulting graphics: a window on a screen (a form), a page being sent to a printer, or a bitmap or image in memory. Each environment presents a drawing surface-a pixel-based representation of the form, image, or page. The defining characteristics of each drawing surface are the same (size, pixel resolution, and color depth) but the manifestation of these characteristics, and the way in which we can control them, is different in each case. In this chapter, we will explore these drawing surfaces, and their differences, in detail-in order that you may produce correct and efficient code for each environment.

In addition to this, we must also have a detailed understanding of the coordinate system used to describe the points of our drawing surface, and through which we define the size, shape, and position of each element of our graphic. When writing GDI+ code to produce complex and intricate graphics-and particularly when producing custom controls-you will find that a single misplaced pixel can greatly impair the visual impact of your work. By gaining an understanding the coordinate system of the surface (and some of the side issues, such as anti-aliasing), you will come to know exactly which pixels will be affected when you invoke a drawing operation in your code-and consequently, you will be able to produce precise and effective graphics.

Thus, the structure of this chapter will be as follows:

  • We'll start with a dissection of drawing surface characteristics, and their singularities in each environment, with particular emphasis on the use and definition of color in GDI+.

  • Then we'll briefly overview how to obtain an instance of the Graphics class, which encapsulates our drawing surface, in each environment.

  • Finally, we'll investigate the coordinate system implemented in GDI+, and of some of its limitations.

These subjects form the foundation of any study of GDI+, and will put us in a good position to study the Pen and Brush classes in Chapter 3, and indeed, all the subjects in the subsequent chapters.


Drawing Surfaces

A drawing surface is a raster-based abstraction of a window on a screen, or a page on a printer, or an image in memory. When we say that it is raster-based, we mean that it is composed of pixels arranged in a two-dimensional grid. A pixel is the smallest element of a drawing surface that we can directly manipulate, and we address individual pixels by using a pair of coordinates in the form (x, y). These are generally called Cartesian coordinates.

The word pixel was originally derived from the words ‘picture element'.

In order to appreciate the characteristics of a raster-based system, it helps to compare it to other systems that are not raster-based. So let's begin this section by comparing the notion of a raster-based system with that of a vector-based system.

Raster-based and Vector-based Drawing Surfaces

When we draw a line in a raster-based system, the overriding characteristic is that the line is represented by the coloring of certain pixels in our grid. We can define the line by expressing exactly which pixels are colored.

By contrast, in a vector-based system, the action of drawing a line is quite different. In this case, the line is represented as an entity that starts at a well-defined point, has a specified length, travels in a specified direction (specified by an angle or path), and is infinitely thin.

To highlight the distinction between these two systems, let's demonstrate them. Here's a representation of a vector-based graphic, which includes a couple of straight (infinitely-thin) lines and a curve:

Click To expand

When we render the same image on a raster-based display, it might look something like this:

Click To expand

The effectiveness of our conversion from vectors to pixels is dependent on the pixel resolution of the raster-based drawing surface-that is, the size of the pixels. In this case, the second image is very low-resolution: the pixels are relatively large and so the image is not a high-quality representation of the original vector representation. But this example serves to illustrate the difference between vector-based and raster-based systems.

These days, almost all devices are ultimately raster-based. Often, we pretend for programming purposes that they are vector-based-because it's easier to describe our images using vector-based techniques, and to depend on some conversion mechanism to turn it into a raster-based representation at the time we wish to project the image description to a drawing surface.

A few years back, vector-based displays were more common than they are now. Many a happy hour could be spent defining each vector programmatically for a vector-based display; complicated electronics in the CRT would direct the beam in such a way that it would draw the vectors that the programmer specified. Anyone who has played the original Battlezone or Asteroids arcade machines has witnessed vector graphics first hand. Large pen-based plotters were also fundamentally vector-based.

But the important technique used today is to write graphics code primarily using vector coordinates, and have these vectors displayed on a raster-based device. Indeed, many of the drawing operations in GDI+ are expressed in terms of vectors. GDI+ then takes the vector expression and renders it to the raster-based drawing surface.

The Characteristics of a Drawing Surface

There are three main characteristics of any drawing surface: its size (width and height), its pixel resolution, and its color depth. These characteristics play a key role in determining how our method calls affect the drawing surface itself.

In addition, these characteristics play a part when we draw first to an Image, which is a drawing surface, and then draw that Image to either a Form or a page on a printer. For example, if we have a very high resolution bitmap, and we draw it on a low resolution drawing surface, the bitmap will be scaled and therefore appear much larger.

Let's examine each of these characteristics a little more closely.

The Size of a Drawing Surface

This is the simplest of the three characteristics listed above. The size of a drawing surface is defined by the number of pixels horizontally and vertically that can be addressed by the drawing surface. For example, the raster-based image we saw at the beginning of this chapter is drawn on a grid of 26 pixels horizontally by 14 pixels vertically.

We can query the drawing surface to determine its size, before we begin drawing (through the X and Y properties of the ClientRectangle of the drawing surface, which we'll meet later in this chapter). Sometimes we can directly change the size, and sometimes we can't-this depends on the type of the drawing surface. We'll see more on this shortly.

We can also find out the resolution of the drawing surface. In GDI+, we use an instance of the Graphics class to encapsulate the drawing surface. Whatever the type of the drawing surface, we can always query the Graphics object to find out the resolution of the drawing surface.

The Resolution of a Drawing Surface

In GDI+, the resolution of our drawing surface is always expressed in units of pixels-per-inch (PPI) or dots-per-inch (DPI). If a surface has a resolution of 72 DPI, then we should be able to examine a 1-inch square of the surface and find that it is composed of a grid of 72 pixels in the horizontal direction by 72 pixels in the vertical direction. It seems a little bit arbitrary that we don't have an equivalent way to specify resolution in metric terms, but it is understandable, in that it would complicate the programming interface significantly.

It's worth noting that these days, pixels are square, and so the resolution is ‘the same' in both the horizontal and vertical directions. There was a period of time back in the 1980s, when some screen devices used rectangular pixels, and this was a bothersome situation to deal with.

Screen Resolution

The screen is assumed to have a resolution of 96 DPI, regardless of the actual resolution of the screen. This is because historically, there has been no way to query the graphics card driver and determine the actual resolution of the screen; indeed, in the past, the graphics driver didn't even know the screen's resolution. Any size of screen can be hooked up to an analog VGA output, and the user can adjust the settings of the physical screen device to change the dots-per-inch, but the graphics card is not aware of these adjustments.

This situation may change with support of more and more plug-and-play monitors that inform the graphics card driver of their size. Knowing the size of the monitor, and the current dimensions of the display (1024×768, 800×600, etc.) provides enough information to be able to calculate the resolution.

Printer Resolution

Every printer device also has a resolution. The user can select their desired resolution; alternatively, we can force resolution programmatically at the time of printing. We'll investigate this subject in more detail in Chapter 9.

The Color Depth of a Drawing Surface

Many operations in GDI+ require a color. Whenever we draw a line, a rectangle, text, or whatever, GDI+ needs to know what color(s) we want to use for that drawing operation. The color depth of a drawing surface defines how many colors can be stored in each pixel.

In GDI+, each pixel describes a color that is made up of a combination of red, green, and blue (RGB) components. There are 256 levels of intensity for each of these three constituent colors (represented by values in the range 0 to 255), and thus each pixel can contain one of 256×256×256=16,777,216 possible colors.

In order to describe all of these colors, our drawing surface must support a 24-bit color value (this is a true color system). If it does, then colors are rendered on the drawing surface as we specify them in our program. However, many display systems do not support 24-bit color-many displays use only 16-bit color (65,546 colors) or 8-bit color (256 colors). If we're drawing to a printer, it might be that the printer does not support color at all, or that it doesn't support a 24-bit color depth. And we can create images with a wide variety of color depths, too. For example, we can create monochrome images (in which only black and white are supported) and 8-bit images (in which only 256 colors are supported).

Dithering

In GDI+, the default drawing model uses 24-bit color depth. If the drawing surface doesn't allow for 24-bit color depth, then GDI+ sometimes restricts the types of graphical operations that can be done, and sometimes takes care of the incompatibility by translation of colors-a process known as dithering.

In the dithering process, GDI+ uses alternating colors in adjacent pixels to give the closest possible approximation to the color that the display is not capable of rendering. The following screenshot shows a highly magnified view of a gradient that is dithered:

Click To expand

Dithering is not really a pleasing effect. To avoid dithering, we can call a method of the Graphics class, GetNearestColor, which will return a color that the display is capable of rendering without dithering. In the future, this issue will be less of a problem since even the cheapest computers will have the capability to display at least 16-bit color depth.

The Color Structure

In GDI+, colors are encapsulated in the Color structure. There are three ways we can employ the Color structure, and we'll show you them here. First, we can create a new instance of the Color structure by passing RGB values into a static function in the Color structure, like this:

Color c = Color.FromArgb(100, 100, 255);
Brush b = new SolidBrush(c);
g.FillRectangle(b, ClientRectangle);

Thus, we specify the exact color by defining the intensity of each of the three components of the RGB scheme. As we've already discussed, the default drawing model uses 24-bit color, and the values passed into the three arguments of the FromArgb method must be between 0 and 255.

Second, there are 141 pre-defined colors that we can reference by name. We use these colors by accessing public properties in the Color structure, as follows:

Color c1 = Color.LavenderBlush;
Color c2 = Color.Red;

The third way to specify a color is to break it down into the following three components, which are collectively known as the HSB model:

  • Hue- this is the actual color, based on its wavelength. Hue is defined by a floating-point value between 0 and 360-this represents an angle, in degrees, which in turn represents a color taken from a color wheel. (A color wheel is a disc that contains all possible hues: there's a 1-to-1 relationship between the colors of the visible spectrum and the radius lines of the disc.)

  • Saturation- this is also known as color intensity. A high-saturation color will appear to be very pure. A low-saturation color will appear to be washed-out. The saturation level is defined by a floating-point number, with a value between 0 and 1. The saturation level has an inverse relationship with the purity of the hue. The more other colors ‘interfere', the more the color will tend towards white or gray and thus appear washed-out.

  • Brightness- this is the relative lightness or darkness of a color, expressed as a floating-point number between 0 (black) and 1 (white).

The Color structure contains static methods that allow us to get these components: GetHue, GetSaturation, and GetBrightness. The following shows how we could use these methods:

Color c = Color.Blue;
float h = c.GetHue();
float s = c.GetSaturation();
float b = c.GetBrightness();

The HSB model of colors is not used much in programming; it is more common in graphic design and other such disciplines. When making custom controls, primarily we will use the colors as supplied by the system preferences set by the user.

The Alpha Component

Colors in GDI+ have an additional component, the Alpha component, which we can use to control the transparency or opacity of a color. This allows us to create fade-in/fade-out effects (such as the fade transition effect for menus and tool tips in Windows 2000 and later), and watermark effects that we can draw over top of other graphics. The Alpha component is specified by an integer value between 0 and 255-0 being completely transparent, and 255 being completely opaque. If we omit the Alpha value, GDI+ assumes you want the default value, which is 255.

All colors in GDI+ always have the Alpha component; often it is set to 255, so the color is completely opaque. When we're working on graphical drawing that doesn't have any aspect of transparency, we typically completely ignore the Alpha component of colors; we allow the color to be completely opaque, which is the default behavior.

Different drawing surfaces use the Alpha component differently. While the colors that we use always have an Alpha component, the screen itself does not contain an Alpha component. The Alpha component has an effect on the drawing operation to give the appearance of transparency, but the resulting image on the screen doesn't have any Alpha component. The same is true for printer pages. However, it is a different story for images. Images themselves can contain an Alpha component for each and every pixel in the image. There are ways that we can use Alpha to blend colors when drawing to an image, and there are ways that we can simply transfer the Alpha value of the color to the image, so that the image retains the Alpha component. Subsequently, when the image is drawn, the image will be drawn semi-transparently, based on the value of the Alpha component of the colors of the pixels.

Here's an example that demonstrates how we might use the Color structure and Alpha component. First, we create two Color instances from the Color structure: c1, which is blue and has an Alpha-level of 100, and c2, which is green and has an Alpha-level of 50. Then we draw three filled shapes:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // create two color instances with different Alpha components
    Color c1 = Color.FromArgb(100, Color.Blue);
    Color c2 = Color.FromArgb(50, Color.Green);

    // now draw a red opaque ellipse
    g.FillEllipse(Brushes.Red, 20, 20, 80, 80);

    // now draw a rectangle, filled with the semi-transparent blue
    g.FillRectangle(new SolidBrush(c1), 60, 80, 60, 60);

    // now draw a polygon, filled with the almost-transparent green
    Point[] pa = new Point[] {
                        new Point(150, 40),
                        new Point(90, 40),
                        new Point(90, 120)};
    g.FillPolygon(new SolidBrush(c2), pa);
}

The first shape is an opaque red circular ellipse. The second is a semi-transparent blue rectangle. Note that it overlaps the ellipse, and is drawn after the ellipse (so that the rectangle appears to be layered over the ellipse). Because the rectangle is semi-transparent (with Alpha 100), we can see the edge of the ellipse through the rectangle:

Click To expand

Finally, we draw a 3-sided polygon (a triangle), filled with the almost-transparent green. Again, the overlapping and layering arrangement, combined with the near-transparency of the triangle, means that we can clearly see the parts of the rectangle and ellipse that lie underneath the circle.

The subject of colors is an involved one. The details that we have just presented will be sufficient for building all but the most exotic of custom controls.

Drawing Surfaces in Different Environments

The three characteristics described above (size, resolution, and color) exist in all three types of drawing surfaces (form, printer, and image), but there are significant differences in how the three types of drawing surfaces deal with these characteristics. In this section, we will give a brief overview all three characteristics in a little more detail for each type of drawing surface. We'll meet all of these characteristics and surfaces again, however, in the coming chapters.

Drawing Windows Forms

A form (a window on a screen) is made up of client and non-client areas. The border of the window and the controls that we use to manipulate the window itself (to resize it, minimize it, maximize it, close it, move it, or bring up the window menu) are called the window decorations, and the size of the window includes the space necessary to display these decorations. The size of the client area only includes the area into which we can draw. The size of the drawing surface for a Form is the size of the client area of the window, which is generally not the same as the size of the window itself.

The following figure illustrates the client area and typical decorations of a window:

Click To expand

In the above screenshot, the decorations include the title bar (and all buttons on the title bar), and the border that surrounds the window. It is possible to create a borderless form that would allow us to draw over the entire surface of the window.

The ClientSize Property

When we create a new Form, either programmatically or using the design window in Visual Studio, we specify the Form's Size property, which sets the size of the entire window (including the decorations). Immediately after creating the window, we can set size of the actual drawing surface using its ClientSize property. To change the drawing surface size for a Form, we use code like this:

this.ClientSize = new Size(120, 20);

If we do this during the Load event, the user will not see the window resize because it effectively resets the window size before its Paint event comes through. By contrast, if we include the line in the code for the Paint event handler, then the user will see the window appear at its initial size, and then resize. Note that the client size is given in the form of a Size object.

The ClientRectangle Property

Every form has a ClientRectangle property that represents the window's client rectangle (the white area in the figure above). You'll see this property often in the coming pages. In particular, we use it to fill the drawing surface with a solid color:

private void Form1_Paint(object sender,
        System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
}
Color Depth and Resolution in Forms

We have little control of the color depth of forms. The user sets the color depth of their screen in their display settings, and our form inherits the color depth of the screen.

Screens primarily come in two varieties, of course: cathode ray tubes (CRTs) and liquid crystal displays (LCDs). Both can have a variety of pixel resolutions, as well as a variety of sizes. As we've already said, there is currently no good mechanism by which we can query the type of hardware being used, and hence determine the actual pixel resolution. GDI+ simply defines that the pixel resolution for all display devices (including monitors of all sizes, and regardless of the screen resolution set in the display properties) is 96 DPI.

This has repercussions when we are drawing images (which can have a variety of DPIs) to a form. We'll examine this interaction in .

Drawing Pages for a Printer

When it comes to printers, we cannot programmatically control the size of the drawing surface directly, but we can control it indirectly by changing the paper size or the printer resolution, as we'll see in Chapter 9. In addition, the user can change these characteristics before printing-by using the printer setup dialog box.

Although it may not be obvious, the drawing surface of a printer is, in fact, closely related to the drawing surface of a print preview window. Consequently, we program for these two drawing surfaces in an almost identical manner. The only difference is that each of these two drawing surfaces requires a different method to create it. Note that if the user sets a different paper size for the printer, the new drawing surface size is also reflected in the print preview window.

We'll see how to set and query the drawing surface resolution and size for a printed page and for the print preview window in Printing.

Every printer has a default pixel resolution, but most printers provide a range of possible pixel resolution settings. A typical default pixel resolution is 300 DPI. The user can change their printer resolution through the Page Setup dialog box. In our code, we can query GDI+ programmatically, to determine the printer's resolution. Programmatically, the print preview drawing surface has the same pixel resolution as the current default printer.

As usual, we program with the assumption that the printer and print preview window have 24-bit color depths. The user can set the color depth of their printer through its advanced properties. In addition, we can control color depth programmatically to a certain extent with the printer-we'll defer discussion and demonstration of all this till Chapter 9.

If the target devices do not support the color depth, GDI+ works in conjunction with the printer driver to make the appropriate color mapping.

Drawing Images

The size of the image represented by a GDI+ Image object (that is, the size of the drawing surface) is specified when the Image object is created. If we create the Image explicitly, then we specify the Image size when invoking the Image class's constructor. By contrast, if we read the image from a file (in a format such as BMP, GIF, or JPG), stream, or other source into an Image object, then the size is specified by the source itself.

We can explicitly set the pixel resolution of an Image object to any desired DPI. If we read the image from a file, stream, etc., then the resolution of the image is determined by the source. In .

Image objects have a wide variety of color depths, which vary from 1-bit for a monochrome bitmap, to 16-bit, 24-bit, 32-bit, 48-bit, or 64-bit. Alternatively, the pixel color for the Image object can be set via a color-indexed value, which means that it uses values that are indexed into colors in the system color table. Image objects can also include an Alpha component in the color definition. This means that we can make portions of an image completely or partially transparent.

We set the color depth of an image at the time of image creation. It's important to understand, however, that when we save an image to a particular format (such as BMP, JPG, or GIF), the framework may change the color depth of the image as it is saved.

A Summary of Drawing Surfaces

The following table summarizes the four types of drawing surfaces, and the characteristics of image size, resolution, and color depth. We've included the print preview as a separate surface, although in many ways it's related to the printer surface:

 | Image Size | Resolution | Color Depth
Forms | Comes from the size of the client area of the window | 96 DPI | Color depth may or may not be 24-bit. GDI+ renders to the display using dithering if necessary.
Printers | Comes from the printer driver. We can change the drawing surface size by changing the page size of the printer, if this is supported by the printer. | Comes from the printer driver. We can change this if the printer supports different resolution values. | Color depth may or may not be 24-bit. GDI+ and the printer driver are responsible for rendering to the printer.
Print Preview | We can change the image size by changing the page size. | Based on the DPI of the printer. | Color depth is the same as for forms. GDI+ renders to the display using dithering if necessary.
Images | Specified upon creation of the image. If we read the image from a file, the image size is also read from the file. | If we create an image programmatically, resolution is 96 DPI by default. If we read the image from a file, resolution is also read from the file. We can change the resolution at any time. | Images can have a wide variety of color depths, including monochrome, grayscale, indexed into a color map, and 16-bit to 64-bit. In addition, we can set the Alpha component for colors.

That completes our investigation of the different drawing surfaces presented in each environment. However, before we can draw to any drawing surface, we must get an instance of the Graphics class.

The Graphics Class

Any time that we wish to draw to a drawing surface, we must get an instance of the Graphics class. The manner in which we obtain our Graphics object will vary depending on our target environment (form, printer, or image).

Creating a Graphics Object for a Form

If we are drawing to a form in response to a Paint event, then we will be handed a Graphics object-it is one of the arguments passed in via the PaintEventArgs argument of our event handler:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    ...
}

If we are responding to some other event, perhaps to a keyboard or mouse event, we can get a Graphics object from the form by calling the CreateGraphics method.

private void Form1_MouseMove(object sender,
                             System.Windows.Forms.MouseEventArgs e)
{
    Graphics g = this.CreateGraphics();
    // 'this' refers to an object of type Control
    ...
}

There are not many situations in which we would want to create a Graphics object in this fashion. However, there are certain circumstances where we do. We explore this in .

Creating a Graphics Object for a Printer or Image

When printing or opening a print preview window, GDI+ again hands Graphics objects to an event handler-in a similar way to what we saw when we acquired a Graphics object in response to a Paint event. We'll avoid the details of this for now, and defer discussion on how to implement printing until Chapter 9.

If we have an Image object, we can get a Graphics object from it by calling the static method Graphics.FromImage. Then, we can draw to the image by drawing to this Graphics object.

Drawing onto a Drawing Surface

The way that we draw onto a drawing surface is to call methods exposed by our Graphics object. To give a flavor of the type of things we can do, the following table lists a selection of the drawing and filling methods provided by the Graphics class. There are some variations on the methods listed here, but this table gives a good representation of the drawing capabilities of the Graphics class.

Note that most of these functions make use of a Pen or Brush object. We use pens for drawing and brushes for filling and painting. We'll discuss pens and brushes in the next chapter.

Method

Description

Clear

Fills the entire drawing surface with a background color, which is specified in the method call.

DrawArc

Draws an elliptical arc. Thus, the arc is a portion of an ellipse-we specify a Pen object, the containing rectangle of the ellipse, and the start and sweep angles.

DrawBezier

Draws a Bézier spline. This is a curve drawn between two specified end-points, whose curvature is controlled by two more specified control points. Thus, we specify a Pen object, two end points and two control points.

DrawCurve

Draws a cardinal spline. This is like drawing a curve that is created by taking a strip of a flexible material and making it pass through a set of fixed points (before the advent of computer-aided design systems, architects and engineers used flexible strips to achieve this in their design drawings).

Note that the path created in this way would depend in part on the ‘flexibility' of the material-a more flexible material would produce tighter bends. This property is also known as the tension.

DrawElipse

Draws an ellipse. We must specify a Pen object and the ellipse's containing rectangle.

DrawIcon

Draws the image in the specified Icon object.

DrawImage

Draws all or part of the specified Image object to the specified area of the drawing surface.

DrawLine

Draws a straight line, using a specified Pen object, between two specified points.

DrawPath

Uses a specified Pen object to draw a path represented by a GraphicsPath object. We'll explore the GraphicsPath class in detail in Chapter 6.

DrawPie

Draws a pie shape. Again, this requires a Pen object, a containing rectangle, and two angles which indicate the arc of the pie.

DrawPolygon

Uses a specified Pen object to draw a polygon, which is represented by an array of points.

DrawRectangle

Uses a specified Pen object to draw the specified rectangle.

DrawString

Draws the specified text-string in a specified font, within the specified containing rectangle. Is overloaded to provide some optional decorative effects.

FillClosedCurve

Uses the specified Brush object to fill the interior of the specified closed cardinal spline curve.

FillEllipse

Uses the specified Brush object to fill the interior of the specified ellipse.

FillPath

Uses the specified Brush object to fill a path represented by a GraphicsPath object.

FillPie

Uses the specified Brush object to fill the interior of a pie shape.

FillPolygon

Uses the specified Brush object to fill the represented by an array of points.

FillRectangle

Uses the specified Brush object to fill the interior of a rectangle.

FillRegion

Uses the specified Brush object to fill the interior of a Region object. We'll explore the Region class in detail in Chapter 6.

Note that some of the descriptions above are deliberately vague. This is because many of these methods are overloaded, which means there are a number of different ways that you can use them to achieve the same thing. For example, where an operation requires a rectangle to be defined, the method is usually overloaded to accept a Rectangle object or a set of defining coordinates for the rectangle. We'll see and discuss many of these methods as we progress through this book, but for a full reference listing, consult the GDI+ section of Microsoft's online MSDN documentation (http://msdn.microsoft.com/library).

Other Functionality from the Graphics Class

The Graphics class encapsulates more than just the functionality of drawing lines and filling shapes. Some of the additional functionality includes:

  • Clipping. We can set a clipping region to be a Rectangle, a GraphicsPath, or a Region. It's useful to set a clipping region when we want to perform operations that affect only part of the drawing surface. After we've set a clipping area, the drawing operations we perform will not affect any pixels that lie outside of the clipping area. We'll cover this in more detail in Chapter 7.

  • Transformations. We can use transformations to cause our drawing operations to be scaled, translated (shifted in the X and/or Y directions), or rotated. We'll look at transformations in Chapter 8.

  • Font metrics. When we draw a text string onto a drawing surface in a given font, the string will occupy a certain amount of horizontal and vertical space. This space is known as the bounding rectangle of the string. When we're drawing complex graphics composed of a number of elements, it's often useful to be able to express the bounding rectangle of text string elements drawn in a given font-for example, to control the exact relative positioning of these elements. We'll discuss text-based facilities in Chapter 4.

  • Interoperability with GDI code: There are GDI+ methods that allow us to draw to a drawing surface using legacy code written using GDI in C++. Thus, if we have some C++ GDI code that performs some drawing operations, and we don't want to re-write it in GDI+, we can create a window in the .NET Framework, then use the Graphics.GetHdc method to get a GDI Device Context (DC), and then use the DC with the C++ GDI code to allow the C++ code to draw into our .NET window.

    There are also methods that allow us to use GDI+ functionality to draw to windows created in legacy applications. Thus, if we have a legacy application and we wish to do some drawing using GDI+ (for example, because of the superior productivity of C#), we can create a window in the traditional way, then get a DC for the window, then use the Graphics.FromHDC method to create a Graphics object from the DC, and finally use GDI+ to draw into the window in our legacy application.

We've said enough about the concept of drawing surfaces to be able to create one and start drawing some elements onto it. Next, we need to consider how we find our way around the drawing surface-without that, we won't have a way to express the drawing operations we want to perform. So in the next section, we must examine the coordinate system used by GDI+.


The GDI+ Coordinate System

When drawing a complicated, intricate graphic, it is important that we draw exactly the pixels that we want to draw. This is less important when working with diagonal lines and curves, but when we're creating custom controls-which typically consist of rectangles, horizontal lines, and vertical lines-the resulting geometrical arrangements can make any misalignment particularly obvious to the human eye. Therefore, in order to draw precision graphics successfully, we must understand the coordinate system of our drawing surface.

As we discussed earlier in the chapter, the drawing surface itself is raster-based-so it is composed of pixels arranged in a two-dimensional grid.

To reflect this, the GDI+ coordinate system consists of imaginary gridlines that run through the center of the pixels. The horizontal and vertical lines are numbered sequentially, starting at zero and incrementing in a downward or right-ward direction. By default, the top-most and left-most gridlines intersect at the pixel point (0, 0). The following diagram illustrates these imaginary gridlines. Here, the squares represent the pixels and the gridlines are numbered along the top and down the left of the grid:

Click To expand

Using the Coordinate System to Draw Lines

So what happens when we want to draw, say, a straight line? Let's look at an example in which we draw a single line between two points. To do this, we can use the Graphics.DrawLine method we met earlier in the chapter:

e.Graphics.DrawLine(Pens.Black, 1, 2, 5, 2);

Here, we've specified the line of starting with pixel (1, 2) and ending at pixel (5, 2). The result of this operation is that five pixels have been drawn black:

Click To expand

It's worth noting how the length and width relate to the gridlines themselves. We've effectively asked for a vector line to be drawn between the points (1, 2) and (5, 2), and so we might have expected to see a line of length 4 pixels-because the points (1, 2) and (5, 2) are exactly 5−1=4 pixels apart. But as you can see from the diagram, the actual length of the line is 5 pixels, not 4. Why?

The difference is explained by the difference between a point and a pixel. A point is infinitely small. A pixel is not infinitely small-it is a square whose sides have a finite length. A line drawn between the points (1, 2) and (5, 2) is, in reality, drawn between the center of pixel (1, 2) and the center of pixel (5, 2)-and does indeed have a length of 4 pixels. But here, GDI+ has decided to mark five pixels black-and so we have a line whose length is effectively measured from the left of pixel (1, 2) to the right of pixel (5, 2)-that's a full 5 pixels in length.

Similarly (and perhaps more obviously), our line has a width of 1 pixel, not 0 pixels. We'll see later in this section how GDI+ uses pen width and other criteria to decide what pixels to mark, and which to leave. Before that, let's briefly look at a couple more operations that use the coordinate system.

Drawing a Diagonal Line

We can draw a diagonal line from point (1, 1) to point (4, 4), with the following code:

e.Graphics.DrawLine(Pens.Black, 1, 1, 4, 4);

As a result, the following pixels will be drawn:

Click To expand
Drawing a Rectangle

The following code will draw a rectangle:

e.Graphics.DrawRectangle(Pens.Black, 1, 0, 5, 4);

The upper left corner of this rectangle will be at the pixel centered at point (1, 0)-as specified by the second and third arguments. The fourth and fifth arguments are the width and height of the rectangle-so the describing rectangle has a width of 5 pixels and a height of 4 pixels. The pen we use is defined by the first argument. The expression Pens.Black (which we'll meet formally in Chapter 3) represents a pen that draws lines that are black and 1 pixel wide. So, the resulting rectangle looks like this:

Click To expand

Again, the defining rectangle has a width of five pixels, but the thickness of the pen ensures that the actual width of the element drawn on the page ‘overhangs' by a half-pixel either side-so it's 6 pixels from end to end.

Similarly, the height of the rectangle appears to be 5 pixels, not 4-because the pen has a finite width (1 pixel), and is therefore not infinitely thin. It is this phenomenon-the width of the pen-which affected the length of the lines we drew earlier in this section, and we'll learn more about the effects of pen width in Chapter 3.

Filling a Rectangle

Having seen how we use our coordinate system to define some simple shapes-and what happens when we try to draw them with a pen of finite width-let's now turn our attention to what happens when we fill the inside of a rectangle. When we fill a rectangle, what we actually do is fill in the entire area of the rectangle, not just draw the perimeter of the rectangle. To demonstrate, let's fill a rectangle that's defined using the same coordinates we used before-with the upper left corner at point (1, 0), a width of 5 and a height of 4-like this:

e.Graphics.FillRectangle(Brushes.Black, 1, 0, 5, 4);

The result is shown overleaf. Given what we saw above, when we drew a rectangle using a pen width of 1 pixel, this isn't what we might have expected. As you can see, the width really is 5 pixels, not 6; and the height really is 4 pixels, not 5! Why is this?

Click To expand

The reason comes in two parts. First, pen width is not relevant now-we're not drawing an outline, we're painting a filled shape (whose outline is infinitely thin). Second, given that pen width is not an issue, GDI+ knows that it is possible to supply a rectangle whose dimensions are exactly those specified (that is, 5 by 4). But what it can't do is set the top-left corner of the filled rectangle into the center of the pixel (1, 0), because the smallest area it can work with is a whole pixel-here, it must either fill in the pixel or leave it unfilled.

So, to satisfy the request, GDI+ fills in the arrangement of pixels shown above-consistently applying a rule which states that if the requested rectangle requires the bottom-right quadrant of a pixel to be filled, then the actual representation will fill the whole of that pixel. Here's how that rule deals with rendering of the pixel (1, 1) in this example:

Click To expand

But it would be better if the top-left corner of the rectangle really was set into the center of pixel (1, 0)-or, at least, if it appeared to be that way. We can overcome this problem using an effect known as anti-aliasing.

Aliasing and Anti-aliasing

Aliasing is the effect that we see as a result of the granularity of the pixels in our drawing surface. A classic example of this is when we draw a diagonal line-the pixel-nature of the surface can only approximate the diagonal line, and so we see a stair-step effect:

Click To expand

It's also an issue when drawing curved lines and text. It is called aliasing because the pixels on the screen are a kind of ‘alias' for the actual shape of our graphic.

Anti-aliasing is a technique that is used to get a better approximation of graphical operations like drawing curves, diagonal lines, and text. Anti-aliasing involves altering the colors of pixels near those which show the stair-step effect-at a sufficiently high resolution, this gives the effect of smoothing, and thus counteracts the raster nature of the surface. The following diagram illustrates the effect. It is an anti-aliased representation of the diagonal line that we showed above:

Click To expand

Note that even after drawing with anti-aliasing, the resulting pixels are still an alias of the theoretical shape of our graphic. (In fact, the term anti-aliasing is a bit of a misnomer; and the term alias-reducing might have been more appropriate.)

Let's see how anti-aliasing affects the rectangle we painted in the previous example-with its top-left corner at point (1, 0), width 5, and height 4. We'll just add one extra line to the code, to turn antialiasing on:

e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.Black, 1, 0, 5, 4);

Here, we're setting the SmoothingMode property of the Graphics object to the value AntiAlias. This value is part of the SmoothingMode enumeration, which belongs to the System.Drawing.Drawing2D namespace. Therefore, for this code to work, we must remember to import the namespace into our code.

Here's the result of this operation:

Click To expand

The color of the edge pixels is determined by their degree of intersection with the actual rectangle defined in the FillRectangle method call. So:

  • The light gray of corner pixels reflects the fact that they have 25% intersection with the rectangle. It is a gray consisting of a 25% tint of black.

  • The rectangle intersects 50% of each of the pixels along the sides of the rectangle, so the gray of those pixels consists of a 50% tint of black.

The actual drawn rectangle, when blown up, looks like this. Of course, this would look much better when seen unmagnified!

Click To expand

Limitations of the Coordinate System

The GDI+ coordinate system and programming model is quite nice to work with in situations where we're not concerned about exactly controlling each and every pixel within our custom control. If we are drawing wire-frames, maps, etc., anti-aliasing gives a nice appearance when viewed at a high resolution. However, this is not the most convenient coordinate system and programming model for writing custom controls-where we want to control the exact color of each and every pixel.

Some people might say that, given today's high-resolution screens, this is not really important. However, if two perpendicular lines that are supposed to meet at a corner don't quite meet, or if one line overshoots the corner by a single pixel, we can easily see this with the naked eye. Moreover, making controls with a 3-dimensional appearance requires a particularly high degree of attention to detail, in order to get the 3D appearance just right. A single pixel off in any direction can spoil the 3D effect.

If we want to address certain pixels unambiguously, drawing in such a way that anti-aliasing is not used at all, then we have two options.

  • We can turn off anti-aliasing, and then pass integer coordinates into all methods, paying close attention to the rules that GDI+ uses when drawing with anti-aliasing turned off. Unfortunately, this is not a good option. It requires a good understanding of the seemingly arbitrary set of rules employed by GDI+ in a non-anti-aliasing environment (in particular, the lower-right quadrant rule that we saw in our aliased rectangle a few pages ago). This will result in code that is hard to read and maintain.

  • We can pass floating-point coordinates into all methods, using coordinates always on the half pixel. With this option, it doesn't matter whether anti-aliasing is on or off-the result is the same.

The second option is the better of the two, so let's take a closer look at it. For example, we can fill a rectangle with the upper left corner at point (1.5f, 0.5f) with a width of 4 and a height of 3, like this:

e.Graphics.FillRectangle(Brushes.Black, 1.5f, 0.5f, 4f, 3f);

The result will always looks like this, regardless of whether anti-aliasing is turned on or off:

Click To expand

However, this technique also has definite drawbacks. In particular, the task of passing floating-point coordinates, always on the half pixel, will result in code that is harder to read, maintain, and explain to other developers.

A Coordinate System for Custom Controls

In short, when designing coordinate systems for application programming interfaces (APIs), there are several different directions that designers can take. The designers of GDI+ took a certain approach that is good for some types of applications, but is not so good for writing custom controls.

So, in Chapter 10, we will take a look at an alternative coordinate system and programming model that is more appropriate for writing custom controls. We'll see how to layer this alternative coordinate system over the top of the existing GDI+ classes. This alternative coordinate system solves the problem of making readable, maintainable graphics code.

Units of Measurement

As we've said, the default unit of measurement for specifying points on the drawing surface in GDI+ is the pixel. Unless we specify otherwise, the values of arguments that we send to methods such as DrawRectangle are assumed to be in terms of pixels. However, there are other units of measurement that we can use, if our needs require them:

Unit of Measure | Physical Size
Display | 1/96 inch
Document | 1/300 inch
Inch | 1 inch
Millimeter | 1 millimeter
Pixel | 1 pixel (this is the default)
Point | 1/72 inch

When writing custom controls, the most useful unit of measurement (by far) is the pixel. Our main concern when writing custom controls is that every pixel is drawn correctly. The problem we encounter when we try to use other units of measurement is that they describe points in our drawing surface that don't lie on a pixel boundary.

For example: if we're working with a display resolution of 96 DPI, and we use DrawRectangle to draw a rectangle of 0.1 inch per side, then the size of the resulting rectangle needs to be 9.6 pixels long. How would this rectangle be rendered on the screen? The only way that GDI+ can draw this is by an approximation-rounding up or down to the nearest pixel. For this reason, if you're working with other units of measurement, much more care is required when drawing custom controls to a screen.

Coordinate Data Types

There are quite a few data types that we use with GDI+ to encapsulate coordinate information. Many of the GDI+ drawing methods take these data types as arguments. Some of these data types are structures, and some are classes. Following is a summary of these types:

Data Type | Class/Structure | Description
Point | Structure | Represents a single point, expressed with integer precision. A Point structure contains X and Y properties.
PointF | Structure | Represents a single point, expressed with floating-point precision. A PointF contains X and Y properties.
Size | Structure | Represents a size, expressed with integer precision. A Size structure contains Width and Height properties.
SizeF | Structure | Represents a size, expressed with floating-point precision. A SizeF structure contains Width and Height properties.
Rectangle | Structure | Represents a rectangle, expressed with integer precision. Rectangles are expressed in terms of position and size (which may be expressed using coordinates and dimensions, or using Point and Size objects).
RectangleF | Structure | Represents a rectangle, expressed with floating-point precision.
GraphicsPath | Class | This class encapsulates a series of connected lines, curves, arcs, pie shapes, polygons, rectangles, and more. We'll explore the GraphicsPath class in detail in Chapter 6.
Region | Class | Describes the interior of a graphics shape composed of rectangles and paths (GraphicPath objects). We'll explore the Region class in detail in Chapter 6.

In this chapter, we will cover the basic data types of Point, Size, and Rectangle in more detail. The GraphicsPath and Region classes are quite a bit more complicated, and are covered in Chapter 6.

The Point, Size, and Rectangle Structures

We can construct a Point structure like this:

Point p = new Point(2, 1);

We can construct a Size object like this:

Size s = new Size(4, 3);

We can construct a Rectangle object from a Point object and a Size object, like this:

Point p = new Point(2, 1);
Size s = new Size(4, 3);
Rectangle r1 = new Rectangle(p, s);

Alternatively, we can construct a Rectangle object by expressing the X and Y coordinates for the top-left corner and the dimensions (width and height) explicitly, like this:

Rectangle r2 = new Rectangle(2, 1, 4, 3);

There are a number of utility methods and operator overloads that make it easy to work with these structures. Let's take a look at them.

Functionality of the Point Structure

The Point structure gives us a fair amount of functionality. We'll cover the more relevant features here:

Method | Description
Offset(dx, dy) | Translates the Point by an offset dx horizontally and dy vertically
+ operator | Using the syntax pt1 = pt0 + sz, this translates the Point object pt0 by a horizontal and vertical distance given by a Size object
− operator | Like the + operator, but translates in the opposite direction
== operator | Compares two Point objects for equality, returning true or false
!= operator | Compares two Point objects for inequality, returning true or false
(Size) | This is a cast operator that converts the given Point object to a Size object

There is an explicit cast that converts from a Point to a PointF structure. All we have to do is to use the cast operator:

Point p = new Point(2, 4);
PointF pf = (PointF)p;

When converting from a PointF to a Point structure, we have three options:

  • The Ceiling method rounds the values to the next highest integer

  • The Round method rounds to the nearest integer

  • The Truncate method truncates any digits to the right of the decimal point

We can demonstrate these three by running the following code:

PointF pf = new PointF(2.5f, 3.5f);
Point p1 = Point.Ceiling(pf);
Point p2 = Point.Round(pf);
Point p3 = Point.Truncate(pf);
Console.WriteLine("pf:" + pf);
Console.WriteLine("p1:" + p1);
Console.WriteLine("p2:" + p2);
Console.WriteLine("p3:" + p3);

The result is the following output:

pf:{X=2.5, Y=3.5}
p1:{X=3,Y=4}
p2:{X=2,Y=4)
p3:{X=2,Y=3}
Functionality of the Size Structure

The Size structure provides the following operators:

Method | Description
+ operator | Using the syntax sz = sz0 + sz1, this returns a Size object whose height and width are the sums of the heights and widths of two other Size objects
− operator | Using the syntax sz = sz0 − sz1, this returns a Size object whose height and width are the differences between the heights and widths of two other Size objects
== operator | Compares two Size objects for equality, returning true or false
!= operator | Compares two Size objects for inequality, returning true or false
(Point) | This is a cast operator that converts the given Size object to a Point object

As with the Point class, when converting from a SizeF to a Size structure, there are the Ceiling, Round, and Truncate methods.

Functionality of the Rectangle Structure

The Rectangle structure has the following methods:

Method | Description
Contains | Determines if the specified Point is contained in the rectangle
Inflate | Creates and returns an inflated copy of the specified rectangle
Intersect | Replaces the current rectangle with its intersection with a second, given rectangle
IntersectsWith | Determines if the current rectangle intersects with a given one
Offset | Translates the location of the rectangle
Union | Returns a rectangle that encloses the union of two specified rectangles
== operator | Compares two Rectangle objects for equality
!= operator | Compares two Rectangle objects for inequality

As with the Point and Size classes, when converting from a RectangleF to Rectangle structure, we can use the Ceiling, Round, and Truncate methods.

Floating-Point Coordinates

We have already briefly discussed the capability of GDI+ to express coordinates in terms of floating-point numbers. If we are using floating-point coordinates, and if we have anti-aliasing turned on, then GDI+ will use anti-aliasing to approximate non-integer coordinates. For example, if we consider a line than runs from point (2, 2) to point (20, 3.4), we would expect to see a line that has a slightly downward slope. We can draw this line with the following code (with anti-aliasing turned on):

Graphics g = e.Graphics;
g.FillRectangle(Brushes.White, this.ClientRectangle);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawLine(Pens.Black, 2f, 2f, 20f, 3.4f);

The screenshot on the left shows what this looks like at fairly high resolution. On the right, we've taken a close-up to see the individual pixels that make up the sloping line:

Click To expand

Admittedly, when you look at the close-up, it's hard to see a gently-sloping line here! But these pixels, when viewed at a high resolution, do give the appearance of a slightly downwards slope because the pixels at the upper left are darker than the pixels at the lower left, and the pixels at the lower right are a little darker than the pixels at the upper right.

You may have noticed that pixel (2, 2) is slightly lighter than pixel (3, 2). This is the correct behavior, in accordance with the anti-aliasing algorithms employed by GDI+. (These algorithms are somewhat complicated, and are beyond the scope of this book.)

There's another good reason to process coordinates in floating-point arithmetic. When performing a large number of transformations on a set of coordinates, if we were to keep the coordinates as integers, significant rounding errors would be introduced due to intermediate results being rounded to the nearest integer. We want to represent intermediate coordinates with high precision to preclude these types of errors.

The Coordinate System Origin

Each window or form into which we can draw has its coordinate space. If we create a custom control that can be put into other windows, this custom control has its own coordinate space. By default, the upper-left pixel of the custom control is (0, 0), regardless of where it is positioned within its parent window.

However, we can change the origin of the window by using transformations, which we will examine in detail in Chapter 8.


Summary

Understanding the concept of a drawing surface is one of the key steps in mastering the GDI+ class library. Once we have an understanding of what a drawing surface is, what it represents, and how it is described, we can better understand how to use them in combination with GDI+'s various methods and classes to create effective graphics. There are also a number of side issues that we needed to understand, so here's a summary of the key points:

  • A drawing surface is a raster-based representation of a form on a screen, or a page being sent to a printer, or an image in memory. It's composed of a two-dimensional grid of pixels, and has three main characteristics: size, resolution and color depth. We represent our graphic by calling operations that are translated into a pixel-by-pixel representation, in which each pixel shows a particular (usually 24-bit) color.

  • One of the interesting aspects of graphical class libraries like GDI+ is the juxtaposition between its vector-based programming interface, which describes the graphic, and the raster-based drawing surfaces on which the graphic is ultimately portrayed. By understanding how vectors are mapped and ‘aliased' to the drawing surface, we will be better able to write code that does exactly what we want it to do.

  • We saw how the GDI+ Graphics class encapsulates the drawing surface abstraction, and we saw some examples in which we used methods of the Graphics class to write drawing code for any drawing surface.

  • We took a look at how GDI+ represents colors-predominantly using the RGB format, but also through a set of colors with predefined names and though the hue-based HSB model-and we also saw how the Color class represents a color. In GDI+, colors can contain an Alpha component, which gives any color an aspect of transparency. While screens and printers only use the Alpha component when drawing on the drawing surface, images can (optionally) retain Alpha information, creating semi-transparent images.

  • We looked at how we can ask GDI+ to apply anti-aliasing algorithms to our graphics, to eliminate the ‘step effect' and thus create the appearance of smoother diagonal lines, curves, and text. We saw several examples of the use of this technique.

  • Finally, we studied the GDI+ coordinate system in some detail. We learned about the advantages and disadvantages of the GDI+ coordinate system. (Chapter 10 introduces an alternative coordinate system, which is intended as a solution to some of the problems of the GDI+ coordinate system.) We saw how we can express points and dimensions using the Point, PointF, Size, SizeF, Rectangle, and RectangleF structures-we'll be using those structures extensively throughout this book.

With all this information under our belt, we have the perfect foundation for the next chapter, in which we'll explore how we use pens to draw and brushes to paint.


Chapter 3: Pens and Brushes

Overview

When it comes to drawing graphics, pens and brushes are the basic tools of the trade. In GDI+, we have two classes that represent these two essential drawing implements. We use the Pen class to draw lines, curves, and outlines of shapes, while we use the Brush class to fill shapes with colors and patterns. If you've read the first two chapters of this book, then you've already seen the Pen and Brush classes being used to demonstrate other concepts of GDI+. They're two classes that we use very often in GDI+.

For this reason, the objective of this chapter is to understand the capabilities of the Pen and Brush classes, and to investigate examples of their use.

These two classes are very flexible, giving us plenty of options that affect the way their operations are performed. So we'll try to get a better understanding of these options, and the exact semantics of their methods-by doing so, we'll be able to harness the full range and power they offer.

So first we'll examine the Pen class. In particular, we'll look at:

  • Pens with different widths

  • Pens that draw full, dotted, and dashed lines, and how to configure these styles

  • Pens that cap lines with arrowheads and other shapes

  • The different ways we can join lines

Then we'll move on, to look at the Brush class. Here, we'll examine:

  • The behavior of texture brushes, and how to use them

  • How linear gradient brushes can be used to give our custom controls a smooth, modern appearance

  • How to use the HatchBrush class to fill a region with a specified pattern

We'll also see how we can use a brush to create a pen. This is a technique that allows us to use brush-type styles to perform pen-type operations, and hence gives us a great deal of extra flexibility in the types of effect we can achieve. Finally, we'll include a few words about how we can expect the creation of Pen and Brush objects to affect the performance of our applications.


Drawing with the Pen Object

We use the GDI+ Pen class to create custom pens. We can specify the Color and Width properties of the pen at the time we instantiate the Pen object. In fact, the Pen class is furnished with a number of other properties, such as the StartCap and EndCap properties (which allow us to add shapes to the start or end of our lines) and the DashStyle property (which allows us to draw dashed and dotted lines). We can adjust the behavior of the Pen object any time after its instantiation, by changing the values of these properties. We'll explore all of these capabilities in this section.

The simplest pen is a solid pen with a width of one pixel. In this case, we are just required to specify the pen's color; so to instantiate such a pen we'd use the class constructor like this:

Pen p = new Pen(Color.Black);

The following example creates such a pen, then uses it to draw a line from the pixel (0, 0) to the pixel (100, 100), and then calls the Pen object's Dispose method to release the resources used by the Pen object:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Pen p = new Pen(Color.Black);
    g.DrawLine(p, 0, 0, 100, 100);
    p.Dispose();
}
Click To expand

The FillRectangle method here simply paints the background of the window (the client rectangle) white; as you can see, it uses a brush to do that. We've just included this operation in order to make the rendered examples look a little cleaner. We'll discuss brushes in detail, later in this chapter.

In fact, there are two constructors for the pen class-the second asks us to specify a Brush object instead of a color. Both constructors give us the option of specifying the pen's width (as we'll see shortly), so in fact there are four different ways to create a Pen class. The first constructor gives us these two:

// Specified color, default width (1px)
Pen p1 = new Pen(myColor);
// Specified color, specified width (px)
Pen p2 = new Pen(myColor, myWidth);

And the second constructor gives us these:

// Specified brush, default width (1px)
Pen p3 = new Pen(myBrush);
// Specified brush, specified width (px)
Pen p4 = new Pen(myBrush, myWidth);

In these lines, myColor represents a Color object (specified in any of the ways we saw in Chapter 2), and myWidth is a value of Float type. Also, in the latter lines, myBrush is a Brush object (this constructor enables us to combine brush effects with pen operations, as we'll see later in the chapter).

We'll see examples of all these as we progress through this chapter (in particular, we'll consider using the second constructor to combine pen and brush capabilities, after we've examined brushes in their own right). Before we do that, let's see some more usage of the Pen class.

Controlling Properties of the Pen

As I mentioned earlier in the chapter, the GDI+ Pen class has a number of properties, which gives us a large amount of control over how our Pen object draws graphic elements. In this section we'll take a look at some of the most commonly used features of the Pen class.

The Width of the Pen

The first and most obvious thing to get right is the way we control a pen's width. Of course, the width of a pen in GDI+ means much the same as it does in real life-thin pens draw fine lines and think pens draw heavy lines. But there are a couple of additional subtleties, and we'll mention those here.

To begin, let's draw a rectangle with a pen width of 1 pixel:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Pen p = new Pen(Color.Black);
    g.DrawRectangle(p, 3, 3, 8, 7);
    p.Dispose();
}

The top-left corner of this rectangle is the point with coordinates (3, 3) and it is 8 pixels long and 7 pixels deep:

Click To expand

In the illustrations here, the grid represents (part of) the client rectangle, and the top-left cell in the grid represents the pixel (0, 0).

A pen of width 1 pixel is used here, because we haven't specified the pen's width (and 1 pixel is the default value used in this case). We can certainly create a pen with a width greater than 1 pixel, by using the optional second argument of the Pen object's constructor. So, let's re-create the rectangle, this time using a 3-pixel-width pen:

Graphics g = e.Graphics;
Pen p = new Pen(Color.Black, 3);
p.Alignment = PenAlignment.Center;
g.DrawRectangle(p, 3, 3, 8, 7);
p.Dispose();

The result of this code is shown here. There are two things to note. First, unsurprisingly, the sides of the rectangle are all 3 pixels wide. Second, although the top-left corner of the rectangle is defined to by the pixel (3, 3), you can see that the apparent corner of the rectangle is the pixel (2, 2). Of course, that's because the pen's width is greater than 1 pixel-it's 3 pixels-and so in this case all the pixels adjacent to the pixel (3, 3) also get drawn black.

Click To expand

In fact, we've used the Pen object's Alignment property here, to control exactly how the pen should present its width on the drawing surface. We've set this property to Center, which dictates that the shape should be drawn as if the center of the pen described the shape.

The best way to appreciate this is to have a look at what happens when we change the value of the Alignment property to Inset:

Graphics g = e.Graphics;
Pen p = new Pen(Color.Black, 3);
p.Alignment = PenAlignment.Inset;
g.DrawRectangle(p, 3, 3, 8, 7);
p.Dispose();
Click To expand

This time, the top-left corner of the rectangle, (3, 3), is the outermost, and the shape has been drawn as if the outside of the pen described the shape (that is, the pen ran along the inside of the shape).

The PenAlignment enumeration used here is part of the System.Drawing.Drawing2D namespace. There are three other values in this enumeration (Outset, Left, and Right). In my tests, I've found them to work in much the same way as Center, although Microsoft's documentation suggests they each have their own distinct effect (with Outset doing the opposite of Inset, and Left and Right creating a sort of left- or right-shadow).

Finally, note that if you don't set the Alignment property, then it defaults to Center.

Drawing Dashed Lines

There are several different ways to create dashed lines and shapes. The simplest way is to set the Pen object's DashStyle property to one of the values provided by the DashStyle enumeration, like this:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Pen p = new Pen(Color.Black, 1);

    p.DashStyle = DashStyle.Dash;
    g.DrawLine(p, 3, 3, 100, 3);
    p.Dispose();
}
Click To expand

The DashStyle enumeration is provided by the System.Drawing.Drawing2D namespace, and in all, provides five such pre-defined styles: Solid, Dash, DashDot, DashDotDot, and Dot. We'll show them all in the next example:

p.DashStyle = DashStyle.Solid;
g.DrawLine(p, 3, 2, 100, 2);

p.DashStyle = DashStyle.Dash;
g.DrawLine(p, 3, 6, 100, 6);

p.DashStyle = DashStyle.DashDot;
g.DrawLine(p, 3, 10, 100, 10);

p.DashStyle = DashStyle.DashDotDot;
g.DrawLine(p, 3, 14, 100, 14);

p.DashStyle = DashStyle.Dot;
g.DrawLine(p, 3, 18, 100, 18);
p.Dispose();

This is what they all look like:

Click To expand
Custom Dashed Lines

These pre-defined dash styles are all very well, but if you've tried viewing the result of the above code fragment at high resolution then you probably found it difficult to tell them apart. For this reason, you might prefer to create your own customized dash styles, by using an array of integers, which describe the pixel-length of the dashes and the spaces between them. Here's an example:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Pen p = new Pen(Color.Black, 1);

    // the following line creates the custom dash pattern:
    float[] f = {15, 5, 10, 5};

    p.DashPattern = f;
    g.DrawRectangle(p, 10, 10, 80, 100);
    p.Dispose();
}

In this example, our dash pattern is specified by the array {15, 5, 10, 5}. We've chosen a four-element array, so our dash pattern will have a cycle of four-a 15-pixel dash, then a 5-pixel space, then a 10-pixel dash, then a 5-pixel space. The pattern begins in the bottom-left corner:

Click To expand

When we assign a value to the Pen object's DashPattern property, its DashStyle property is automatically set to the value DashStyle.Custom.

Dashed Lines with Width Greater Than One Pixel

If the pen's width is set to a value greater than 1 pixel, then the actual length of each dash and space is calculated by multiplying the values specified in the array by the width of the pen. For example, let's change the pen's width to 2 pixels in the above example:

Pen p = new Pen(Color.Black, 2);

// the following line creates the custom dash pattern:
float[] f = {15, 5, 10, 5};

p.DashPattern = f;
g.DrawRectangle(p, 10, 10, 80, 100);
p.Dispose();

Now the pattern array {15, 5, 10, 5} produces alternating dashes of length 30 and 20, separated by spaces of length 10 pixels. You can see this by comparing this result with the previous one:

Click To expand

Arrowheads and Other Line Caps

We can specify how GDI+ decorates the beginning and end of a line by using line caps. In practice this means assigning values to the Pen object's StartCap and EndCap properties. The following code draws a line with a StartCap of LineCap.Round, and an EndCap of LineCap.ArrowAnchor:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.SmoothingMode = SmoothingMode.AntiAlias;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Pen p = new Pen(Color.Black, 10);
    p.StartCap = LineCap.Round;
    p.EndCap = LineCap.ArrowAnchor;
    g.DrawLine(p, 30, 30, 80, 30);
    p.Dispose();
}

This code fragment produces a line that looks like this. Note that we get best results when we use a pen whose width is greater than 1 pixel. This example uses a 10-pixel width pen, which (as you can see) creates a very effective rounded end.

The following table describes the available line caps and their appearances:

p.StartCap = LineCap.ArrowAnchor;
p.EndCap   = LineCap.ArrowAnchor;

p.StartCap = LineCap.DiamondAnchor;
p.EndCap   = LineCap.DiamondAnchor;

p.StartCap = LineCap.Flat;
p.EndCap   = LineCap.Flat;

p.StartCap = LineCap.Round;
p.EndCap   = LineCap.Round;

p.StartCap = LineCap.RoundAnchor;
p.EndCap   = LineCap.RoundAnchor;

p.StartCap = LineCap.Square;
p.EndCap   = LineCap.Square;

p.StartCap = LineCap.SquareAnchor;
p.EndCap   = LineCap.SquareAnchor;

p.StartCap = LineCap.Triangle;
p.EndCap   = LineCap.Triangle;

You can see that the anchor line caps (ArrowAnchor, DiamondAnchor, RoundAnchor, and SquareAnchor) all have a very distinctive appearance, in that they all add a decorative anchor to the end of the line. The others merely ‘trim' the line-end in some way. All of these values belong to the LineCap enumeration, which is part of the System.Drawing.Drawing2D namespace.

Custom Line Caps

We can also create custom line caps by instantiating the CustomLineCap class, and setting the CustomStartCap and CustomEndCap properties of the Pen class. To create a custom line cap, we create a GraphicsPath object with the desired path of our line cap, then instantiate the CustomLineCap class using the GraphicsPath object (which is covered in ). We can set various options regarding scaling, how lines join within the line cap, and so on.

Building custom line caps is not necessary for most custom controls, so we will not delve into it too deeply-but it's useful to know that you can do it if you need to.

Joining Lines

Whenever we perform a single operation that involves the rendering of joined lines, we can set the style in which the lines are joined. To do this, we use values from the System.Drawing.Drawing2D namespace's LineJoin enumeration. There are four values here: Miter (the default), Beveled, MiterClipped and Round. We tend not to use MiterClipped when writing custom controls, and we've been using Miter (by default) in the examples in this chapter. So here, let's just briefly demonstrate the other two.

In the following code, we set our Pen object's LineJoin property to the value LineJoin.Bevel, and then use it to draw a rectangle:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.SmoothingMode = SmoothingMode.AntiAlias;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Pen p = new Pen(Color.Black, 10);
    p.LineJoin = LineJoin.Bevel;
    e.Graphics.DrawRectangle(p, 20, 20, 60, 60);
    p.Dispose();
}

Unsurprisingly, the resulting rectangle has bevelled corners, like this.

Alternatively, we can set the LineJoin property to the value LineJoin.Round:

Pen p = new Pen(Color.Black, 10);
p.LineJoin = LineJoin.Round;
e.Graphics.DrawRectangle(p, 20, 20, 60, 60);
p.Dispose();

Then the rectangle has rounded corners, like this. Once again, you can see that this feature produces the better effects when the pen width is relatively high (but not so high that you can't make out the shape being drawn!). These images are produced with a 10-pixel pen width.

The Pens Class

In Chapter 2, we discussed the Color structure, and saw that it allows us to use the 141 pre-defined colors made available in the Color structure. For convenience, GDI+ provides us with a pre-defined set of 141 colored pens. Each of these pens has width 1, and there is a pre-built pen for every color that is defined in the Color structure.

To access these pens, we use the Pens class, like this:

private void Form1_Paint(object sender,
                        System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    g.DrawRectangle(Pens.MistyRose, 20, 20, 40, 40);
}

The result of this code fragment is a rather pinkish-looking rectangle whose edges have width 1 pixel. As a general rule, if our graphics allow us to use the pens from the Pens class, then we should do so. There are some performance advantages: we'll return briefly to the subject of performance at the end of this chapter.

Disposing of Pen Objects

Note that when we create pen objects using the Pens class, we don't create the pen explicitly-and therefore it's wrong to try to call the Dispose method of the resulting Pen object. If we attempted to do that, as in the following code fragment, then an exception would result:

Pen blackPen = Pens.Black;
g.DrawRectangle(blackPen, 20, 20, 40, 40);
blackPen.Dispose();                  // generates an exception

In general, the rule to follow is this: if you created it, then you dispose of it. If you did not create it, then leave it alone.


Filling with the Brush Object

In GDI+, we use brushes to fill shapes with colors, patterns, and images. The GDI+ Brush class itself is an abstract class, and so we can't instantiate it directly. Rather, the GDI+ API provides the following five classes that extend the Brush class and provide concrete implementations:

Class | Description
SolidBrush | Fills a shape with solid colors.
TextureBrush | Fills the shape with a raster-based image (bitmap, JPG, etc.).
LinearGradientBrush | Fills the shape with a color gradient-a filling that changes evenly from one color to another, in a specified direction and between two specified (parallel) boundary lines.
PathGradientBrush | Fills the shape with a gradient that changes evenly as we move from the boundary of the shape defined by the path to the center of the shape. This type of gradient reminds me of coloring maps in grade school, where we would color the outlines of the countries with a darker color than the center of the countries.
HatchBrush | Fills the shape with various patterns.

Using the Brush Classes

In this section we'll look at some of the features of these brushes in more detail. It seems sensible to start with the simplest of these-the SolidBrush. The following code creates such a brush, and then uses it to fill a rectangle:

SolidBrush b = new SolidBrush(Color.Crimson);
g.FillRectangle(b, 20, 20, 40, 40);
b.Dispose();

This renders a rectangle whose interior is filled with crimson:

Click To expand

There is only one constructor for a SolidBrush, and after we create it, there are no options that can be set.

The TextureBrush Class

A TextureBrush object fills a shape with a raster-based image. It uses an image which comes from an image file such as a .bmp, .jpg, or .png. We can obtain such an image from a file by using the Bitmap class, which is a sub-class of the Image class (we'll discuss the Image class in much more detail in ). To do that, we can use code like this:

Bitmap bmp;
bmp = new Bitmap("alphabet.gif");

In order to demonstrate a few simple examples that involve the TextureBrush class, we need a suitable image-one which has an obvious boundary, and which looks clearly different if we flip (or reflect) it horizontally or vertically. So in these examples, we'll use the image alphabet.gif, which is shown here, and which will ensure the results of our manipulations will be very evident. The image file alphabet.gif is included as part of the sample code for this book, and is available for download at http://www.wrox.com.

For a first example here, let's just draw a simple rectangle, filled using this pattern:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("alphabet.gif");
    TextureBrush tb = new TextureBrush(bmp);

    g.FillRectangle(tb, 20, 20, 200, 70);
    bmp.Dispose();
    tb.Dispose();
}

Here, we first specify the name and location of our raster-based image as an argument to the Bitmap class constructor, and specify the resulting Bitmap object in the constructor for the TextureBrush object. The result is a TextureBrush object that fills things using a tile effect based on the pattern in the image file.

Important

Note that for the example to work, you must place the image file in the same directory as the executable. If you're compiling with debugging information turned on, then Visual Studio .NET places the executable in the /bin/debug subdirectory of your project's directory, so you must place the image there.

Once we have our TextureBrush object, we can use it to create some filled shapes-in this case, a filled rectangle. Here's what the result of the above code looks like:

Click To expand

Notice that the top-left pixel of the rectangle we've painted here is (20, 20). You might be a little surprised to see that we see only part of the alphabet.gif image nestling into the top-left corner of our rectangle. To reason for this is simple. Our TextureBrush object fills the rectangle by using the image as a tile, and arranging copies of the image, like tiles, on the drawing surface-but of course, it needs a starting point for the tiling pattern, and unless we tell it otherwise, it starts the tiling pattern from pixel (0, 0). With that in place, the top-left corner of our rectangle falls in the middle of a tile, and so we see only a part tile in that corner.

The TextureBrush behavior we see above-tiling from the pixel (0, 0)-is a default behavior, and can be overridden by setting a transformation on the TextureBrush object. We defer discussion of transformations until .

Overlapping Shapes

This dynamic of the texture brush allows us to draw lots of shapes using the same brush-and if these drawing operations overlap, the bitmaps that make up the drawing operations will be aligned. We can demonstrate this by adding more overlapping shapes to the example above, for example, if we modify the Paint event so that we draw another overlapping rectangle, as follows:

Graphics g = e.Graphics;
Bitmap bmp = new Bitmap("alphabet.gif");
TextureBrush tb = new TextureBrush(bmp);

g.FillRectangle(tb, 20, 20, 200, 70);
g,FillRectangle(tb, 45, 45, 70, 150);
bmp.Dispose();
tb.Dispose();
Click To expand
Using a Selection from an Image as a Tile

We don't need to use the entire bitmap for our texture brush. The TextureBrush constructor is overloaded to allow us to select a portion of the image, which is then used as the tile when the TextureBrush fills. To demonstrate this, let's adjust the previous example to use the overloaded constructor, as follows:

Graphics g = e.Graphics;
Bitmap bmp = new Bitmap("alphabet.gif");
TextureBrush tb = new TextureBrush(bmp, new Rectangle(0, 0, 25, 25));

g.FillRectangle(tb, 20, 20, 200, 70);
g.FillRectangle(tb, 45, 45, 70, 150);
bmp.Dispose();
tb.Dispose();

You can see that this example just uses a 25-pixel square taken from the top-left corner of alphabet.gif to tile the two rectangles:

Click To expand
Different Tiling Effects

We've seen the basic tiling effect that the TextureBrush achieves. We can use the TextureBrush object's WrapMode property to change the way it uses the tiles. Once again, GDI+ provides us with an enumeration-the WrapMode enumeration-whose values can be used in conjunction with the TextureBrush.WrapMode property to achieve different tiling effects, as we'll see in the following examples.

The WrapMode enumeration is part of the System.Drawing.Drawing2D namespace, and contains five values-Tile, Clamp, TileFlipX, TileFlipY, and TileFlipXY. We'll very briefly compare them all here by using them, one at a time, to fill the entire client rectangle.

First, let's see the effect off setting the WrapMode property to WrapMode.Tile. This is the default, and tiles the bitmap over the entire drawing surface:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{

    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("alphabet.gif");
    TextureBrush tb = new TextureBrush(bmp);

    tb.WrapMode = WrapMode.Tile;
    g.FillRectangle(tb, this.ClientRectangle);
    bmp.Dispose();
    tb.Dispose();
}

There isn't much new to note here (except that we've chosen to fill the client rectangle in these examples, instead of filling a smaller rectangle with specified corner pixels).

Click To expand

If we set the WrapMode property to WrapMode.Clamp, the texture brush doesn't tile the shape at all instead, it clamps the bitmap to the origin of the drawing surface:

tb.WrapMode = WrapMode.Clamp;
g.FillRectangle(tb, this.ClientRectangle);
Click To expand

If a clamped brush is used to fill a rectangle that is larger than the brush, then any portion of the rectangle outside of the image will not be drawn.

If we set the WrapMode property to WrapMode.TileFlipX, the texture brush tiles the bitmap over the drawing surface, flipping every other horizontal bitmap horizontally:

tb.WrapMode = WrapMode.TileFlipX;
g.FillRectangle(tb, this.ClientRectangle);
Click To expand

A similar effect is gained when we set the WrapMode property to WrapMode.TileFlipY. This time, the texture brush flips every other vertical bitmap vertically:

tb.WrapMode = WrapMode.TileFlipY;
g.FillRectangle(tb, this.ClientRectangle);
Click To expand

Finally, the effect of setting the WrapMode property to WrapMode.TileFlipXY gives us an amalgam of the previous two effects-flipping the bitmap in both the vertical and horizontal directions:

tb.WrapMode = WrapMode.TileFlipXY;
g.FillRectangle(tb, this.ClientRectangle);
Click To expand
Changing the Attributes of the Image

Before we move on to study the other types of Brush, it's worth mentioning that GDI+ allows us to affect how an image is drawn-by using the ImageAttributes class to specify certain image attributes. We'll discuss these techniques more closely in ; but we mention it now because the TextureBrush can take advantage of these techniques.

GDI+ allows us to pass an ImageAttributes object to the TextureBrush constructor at the time we create the TextureBrush object. The ImageAttributes object can contain attribute values that affect the way the brush draws the image. To demonstrate this, here's a quick example that uses an ImageAttributes object to manipulate the version of alphabet.gif that is painted on the screen:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Bitmap bmp = new Bitmap("alphabet.gif")
    Graphics g = e.Graphics;
    // Use a color matrix to change the color properties of the image
    float[][] matrixItems = {
                        new float[] {0.2f, 0, 0, 0, 0},
                        new float[] {0, 0.8f, 0, 0, 0},
                        new float[] {0, 0, 1, 0, 0),
                        new float[] {0, 0, 0, 1, 0},
                        new float[] {0, 0, 0, 0, 1}};
    ColorMatrix colorMatrix = new ColorMatrix(matrixItems);

    // Create an ImageAttributes object and set its color matrix
    ImageAttributes imageAtt = new ImageAttributes();
    imageAtt.SetColorMatrix(
                        colorMatrix,
                        ColorMatrixFlag.Default,
                        ColorAdjustType.Bitmap);

    // Create a TextureBrush object using the alphabet.gif image,
    // adjusted by the attributes in the ImageAttributes object
    TextureBrush tb = new TextureBrush(
                bmp,
                new Rectangle(0, 0, bmp.width, bmp.Height),
                imageAtt);
    tb.WrapMode = WrapMode.Tile;
    g.FillRectangle(tb, this.ClientRectangle);
    bmp.Dispose();
    tb.Dispose();
}

Note that this example makes use of objects and enumerations that belong to the System.Drawing.Imaging namespace.

The effect of all this is that the TextureBrush fills the client rectangle with a tile based on the image alphabet.gif, but whose background color is blue, not white. Unfortunately the color doesn't come out too well in grayscale, but this gives you an idea of what to expect. We'll talk in more detail about color matrices and the ImageAttributes object in Chapter 5.

Click To expand
Tiles and Transformations

There are facilities in the TextureBrush class to do transformations on the image before the brush paints it to the drawing surface.

Tiles and Anti-aliasing

Texture brushes also behave in accordance with the coordinate system and anti-aliasing rules we discussed in Chapter 2. You may recall that the coordinate system model of GDI+ is such that coordinates refer to gridlines that run horizontally and vertically through the center of the pixels. The question here is: What would it mean to draw an image at a location half way through a pixel? If antialiasing is turned on, and we create a texture brush, and then draw it on integral coordinates, the edges of the brush's bitmap will be anti-aliased. However, it is only the edges that are anti-aliased-the internal portion of the bitmap is not affected by the anti-aliasing option.

That said, drawing images with anti-aliasing is a dubious activity at best, and we will almost never want to do it.

The LinearGradientBrush Class

A LinearGradientBrush object fills shapes with a linear color gradient. In its simplest terms, a color gradient consists of a gradual color change between two specified colors as we traverse a straight-line path that tends a specified angle. We'll demonstrate this with a couple of examples in a moment.

In fact, the LinearGradientBrush class encapsulates two-color gradients and multi-color gradients, as we'll see in this section. To describe a simple, two-color linear gradient brush, there are different ways to describe the gradient line: we can specify a pair of points, or a rectangle and an angle. The LinearGradientBrush constructor is overloaded to take in these different possibilities, and we'll see them here.

Using Two Points to Describe a Gradient

First, let's instantiate a LinearGradientBrush object by providing two points (a start point and an end point) and two colors (a start color and an end color):

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;

    LinearGradientBrush lgb = new LinearGradientBrush(
                                         new Point(0, 0),
                                         new Point(50, 10),
                                         Color.White,
                                         Color.Black);
    g.FillRectangle(lgb, this.ClientRectangle);
    lgb.Dispose();
}

Here's what we see when we run this application. Let's consider how this works. First, note that the two points specified describe the gradient line-here, it starts at the pixel (0, 0) and ends at the point (50, 10). Of course, you can't see the gradient line on the screenshot, but you can imagine it. Second, note that at the start of the gradient line (the top-left corner of the client rectangle), the brush has painted the start color, White. At the end of the line, the brush has pained the end color, Black. In between, the color changes gradually from white to black as we look along this imaginary line.

Click To expand

Finally, note that this pattern is repeated in two directions-not horizontally and vertically, but in directions parallel and perpendicular to the direction of the gradient line. The effect is one of stripes-the stripes run perpendicular to the gradient line.

Using a Rectangle to Describe a Gradient

Another way to define the gradient is to specify a rectangle and an angle in degrees. For example:

LinearGradientBrush lgb = new LinearGradientBrush(
                                     new Rectangle(0, 0, 50, 50),
                                     Color.White,
                                     Color.Black,
                                     75f);
g.FillRectangle(lgb, this.ClientRectangle);
lgb.Dispose();

In this case, the first color is used on the upper left corner of the rectangle; the second color is used on the lower right corner of the rectangle. In between, the color changes as per the gradient specified by the fourth argument, which represents the angle that the gradient line tends to the horizontal.

Click To expand

Note that the gradient line is specified by the angle in the fourth argument, not by the positions of the two opposite corners of the rectangle! Note also that the angle specified represents the angle that the gradient line makes with the horizontal, in degrees. If we were to specify an angle of zero, the gradient line would be horizontal, and the stripes would appear to be vertical, like this:

Click To expand
Other Options for the LinearGradientBrush

At the beginning of the section, we mentioned that a LinearGradientBrush object can create both two-color and multi-color gradients. A multi-color gradient is like a sequence of top-to-tail two-color transitions, which pass through certain specified colors at certain points along the gradient line. We describe this sequence by specifying colors in the sequence, and how far along the gradient line we want each color to occur. If you want to try this, you should look at the LinearGradientBrush class's SetInterpolationColors method-it expects an array of the colors in the gradient, an array of numbers representing the positions, and an integer count of the total number of set colors in the sequence.

There are other options that we can set when using gradient brushes. For example, we can:

  • Control how one color transitions to another color

  • Enable and disable Gamma correction. This is a complicated subject, but the essence of it is that we could define a gradient between two colors, and with Gamma correction, the gradient would have the appearance of equal brightness at all points along the gradient

  • Define a multi-color gradient, which can be used to create a 'chrome' look-like the effect we see when we look at a shiny bathroom faucet.

These effects are quite advanced, and outside the scope of this book. With the increase in processing power, and the increase in users' expectations for a slick, fancy look, we can use gradients to good effect when building custom controls. But keep in mind that these effects can be overused. In general, we should stick to the norms of the Windows user interface. The Windows XP look contains some good examples of gradients that are subtly and tastefully implemented.

The PathGradientBrush Class

The PathGradientBrush object also provides us with a brush that fills a shape with a color gradient. In this case, we specify a path and two colors, and the resulting color gradient begins at all points on the specified path, and ends at a point which is deemed to be the center of the shape. We'll see an example in a moment.

In order to use the PathGradientBrush class effectively, we use it in conjunction with the GraphicsPath class. We'll cover the GraphicsPath class in detail in Chapter 6, GraphicsPaths and Regions, but for the purposes of this section we'll give a very brief overview here.

The GraphicsPath Object

We use the GraphicsPath class to describe a shape composed of multiple line segments. We say that a GraphicsPath object is closed if the path's end point is the same as its start point; otherwise, we say that the GraphicsPath object is open. We can convert an open GraphicsPath object to be closed, by connecting the last point to the first point. We can do this explicitly by calling the object's CloseFigure method (as we'll see in an example below).

When we put together a GraphicsPath object, we first construct it, and then add segments to it. We have a large variety of operations that allow us to add different types of segments to the path. In the following example, we create a GraphicsPath object, add three line segments to create an open path, then close the path, and then draw the path with a pen:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    GraphicsPath gp = new GraphicsPath();

    gp.AddLine(10, 10, 110, 15);
    gp.AddLine(110, 15, 100, 96);
    gp.AddLine(100, 96, 15, 110);
    gp.CloseFigure();

    g.FillRectangle(Brushes.White, this.ClientRectangle);
    g.DrawPath(Pens.Black, gp);
    gp.Dispose();
}

The result looks like this:

Click To expand

As an aside, it's worth noting that the second AddLine method call, highlighted below, is not technically necessary:

GraphicsPath gp = new GraphicsPath();
gp.AddLine(10, 10, 110, 15);
gp.AddLine(110, 15, 100, 96);
gp.AddLine(100, 96, 15, 110);
gp.CloseFigure();

If we add two segments that are not joined, the GraphicsPath class will automatically add a segment to join them. In the example above, for clarity, we added all segments explicitly-except for the last one, which is added by the call to CloseFigure.

This is not the whole story as far as the GraphicsPath class is concerned, but it's sufficient information to enable us to take a meaningful look at the PathGradientBrush class.

Creating a PathGradientBrush from a GraphicsPath

Now, we can create a PathGradientBrush from the GraphicsPath object:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    GraphicsPath gp = new GraphicsPath();

    gp.AddLine(10, 10, 110, 15);
    gp.AddLine(110, 15, 100, 96);
    gp.AddLine(100, 96, 15, 110);
    gp.CloseFigure();

    g.FillRectangle(Brushes.White, this.ClientRectangle);
    g.SmoothingMode = SmoothingMode.AntiAlias;

    PathGradientBrush pgb = new PathGradientBrush(gp);
    pgb.CenterColor = Color.White;
    pgb.SurroundColors = new Color[]
    {
        Color.Blue
    };
    g.FillPath(pgb, gp);

    g.DrawPath(Pens.Black, gp);
    pgb.Dispose();
    gp.Dispose();
}

Here, we pass the GraphicsPath object to the constructor of the PathGradientBrush class. Then we need to set the PathGradientBrush.CenterColor property-here, we've set it to white (using the Color class). The next statement is interesting:

pgb.SurroundColors = new Color[]
{
    Color.Blue
};

We pass an array of colors for the SurroundColors property. In this case, we created an array with only one color in it, so when we run the application, the PathGradientBrush paints blue at the edge of the path, transitioning to white at the center. Our code paints the following figure:

Click To expand

To see the full effect of the color array in the previous code fragment, let's modify our example to set the SurroundColors property to an array that contains several colors, like this:

pgb.SurroundColors = new Color[]
{
    Color.Blue,
    Color.Red,
    Color.Green,
    Color.Yellow
};

You'll need to try this out for yourself to appreciate the effect we've created here, because it doesn't show up too well in grayscale-what you'll see is a similarly-shaped figure, which is white in its center but whose colors vary around the edge of the graphic. In fact, the four corners (from top-left, clockwise) are blue, red, green, and yellow.

Note that we can't set the SurroundColors property to an array that contains more colors than there are segments in the GraphicsPath object. If we do so, we will get an exception.

The HatchBrush Class

The HatchBrush class provides us with a brush that can fill a shape with a variety of patterns. The patterns for hatch brushes are always two-color patterns, based on a 16-pixel by 16-pixel repeat. We set the hatch style via the HatchStyle enumeration, which belongs to the System.Drawing.Drawing2D namespace.

The following code sets the HatchStyle to the Cross pattern, and then fills the client area of the window:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;

    HatchBrush hb = new HatchBrush(
                      HatchStyle.Cross,
                      Color.White,
                      Color.Black);
    g.FillRectangle(hb, this.ClientRectangle);
    hb.Dispose();
}

When run, the example looks like this:

Click To expand

There are only three options that we can set when creating a hatch brush: the foreground color, the background color, and the hatch style. There are 56 different hatch styles-too many for us to demonstrate them all here! For more information on their names and appearance, see the System.Drawing.Drawing2D.HatchStyle enumeration in the .NET Framework documentation.

Hatch brushes are particularly useful when printing. In this scenario, we can query the printer and find out whether it is capable of printing only in black and white, or whether it can print in color. If printing is limited to black and white, then hatch brushes make a useful alternative to colors when printing things like bar charts.

The Brushes Class

GDI+ provides us with a Brushes class, which (like the Pens class we discussed earlier) provides us with a set of pre-defined SolidBrush objects. There are 141 such-one for each pre-defined color. In fact, we've already used this class several times in this chapter to fill the background of the client area of our window with white, like this:

g.FillRectangle(Brushes.White, this.ClientRectangle);

Each time we requested a brush from the Brushes class, we did not explicitly create the brush, so we should not call the Dispose method on it. It seems that the intent of the framework designers is that the framework does not dispose of solid brushes acquired from the Brushes class until the application terminates. It probably makes good sense; these brushes will typically be used often within an application, and the resource cost is minimal, so eliminating the creation/disposal cycle as far as possible will enable better performance.

Creating a Pen from a Brush

Having discussed the key features and differences of both pens and brushes, it's worth taking a look at how we can use brush-type effects to perform pen-type operations. Clearly, this will not be very interesting if your line is only 1 pixel wide, but when we are working with a wider pen, we can work as though we are filling the mark described by a pen with an effect that we can only achieve with a brush.

In the following example, we'll create a hatch brush, then create a pen from the brush, then draw a rectangle with the pen. We'll give the pen a width of 8 pixels, which is wide enough to ensure we can see the pattern of the hatch brush where the pen draws:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    HatchBrush hb = new HatchBrush(
             HatchStyle.WideUpwardDiagonal,
             Color.White,
             Color.Black);
    Pen hp = new Pen(hb, 8);
    g.DrawRectangle(hp, 15, 15, 70, 70);
    hb.Dispose();
    hp.Dispose();
}

To create the Pen object here, we've used one of the overloaded Pen constructors we saw near the beginning of this chapter. This constructor expects two arguments: the first argument is a Brush object and the second (optional) argument is an integer specifying the pen's width. When run, the example looks like this:

Click To expand

A Note about Performance

Most professional programmers have had exposure to pens and brushes in the context of graphical drawing. Some toolkits, such as GDI (the predecessor to GDI+), provide methods that combine the shape-drawing tasks of a pen and the shape-filling tasks of a brush into a single operation. However, as you've seen, in GDI+ these operations are completely separate. If we want to draw an outline and fill the interior of it, we simply do it in two distinct operations.

As a developer, this separation of the drawing and filling tasks might cause you to be concerned about the potential effects on performance-particularly if each shape we draw requires two calls into the Windows API instead of one. As it turns out, the cost of marshaling an additional call into the Windows API is negligible, particularly when we compare this to operations such as changing pens and brushes. If the separation of the drawing and filling tasks enables us to change pens and brushes less often, then the net result is a huge performance gain. Experiments with the .NET Framework show a similar performance profile-changing pens is expensive compared to drawing operations with a pen.

I am reminded of an occasion when I needed to get a significant performance gain in some drawing code of an elaborate grid control. My first approach was to implement some sophisticated (or rather, complicated) algorithms and data structures that would eliminate some work at drawing time, and thus give me the desired performance improvement. These optimizations helped, but not nearly as much as I wanted.

Frustrated, I spent some time profiling the various operations in the underlying toolkit. It was then that I realized the issue-the process of changing properties in pens and brushes is very slow. With this, I made a very simple modification to the code. I arranged that the code first drew all of the cells in the grid that had a white background with black text (which was the most common case); and then drew all of the exceptional cases with differing fonts, colors, and the like. I immediately got a performance increase of nearly an order of magnitude!

Aside from this specific lesson about changing pens and brushes in GDI+, there's a broader lesson that I took away from this experience: when profiling and optimizing code that makes extensive use of any underlying toolkit (not just graphical toolkits), be suspicious of the performance profiles of the various methods in the toolkit.


Summary

In this chapter, we've covered the essential details behind the fundamental GDI+ concepts of pens and brushes.

In GDI+, we generally use Pen objects for drawing shapes. We've seen that we can specify what color the pen should draw in, and we have the option of indicating what width the pen should be. We've also seen that the Pens class provides us with a set of 141 pre-defined colored pens, all of width 1 pixel.

Also, we use Brush objects for filling operations. The Brush class itself is an abstract class, which manifests itself in the form of five special types of brush: SolidBrush, TextureBrush, LinearGradientBrush, PathGradientBrush, and HatchBrush. We looked at how the TextureBrush uses an image file to create a textured effect, and how the LinearGradientBrush and PathGradientBrush use the color gradient technique to achieve their effects.

In passing, we saw that the Pen class is not the only class that allows us to draw shapes. In fact, in our study of the PathGradientBrush we made use of the GraphicsPath class. We'll meet the GraphicsPath class again in Chapter 6.

Moreover, we can perform single operations that combine the visual capabilities of a brush with the drawing capabilities of a pen-this gives us a technique that is just like drawing with a brush.

We've achieved the purpose of this chapter, which is to cover the basic capabilities of the pen and brush in GDI+. This basic set of functionality is enough in itself to enable us to draw powerful, effective graphics. But it gets even better when we combine these capabilities with other GDI+ functionality. In the next chapter, we'll take a look at how we can use text and fonts to enhance these graphics even further.


Chapter 4: Text and Fonts

Highlights

It's hard to think of an application that doesn't need to present text in some format or other. Some applications don't use much text, but most applications require at least some-even if it's just in dialog boxes. If we're building an elaborate custom control, such as a spreadsheet, a tree control, or network diagram, our use of text and fonts may be very extensive.

Therefore, if we're going to write effective custom controls, it's important for us to be able to write efficient and effective code for drawing text. To do this, we need to be able to identify exactly what we want our custom control to do, and we need a good understanding of the text rendering capabilities of GDI+.

This chapter focuses mostly on the text-rendering functionality provided by GDI+. Hopefully, during the course of the chapter, we will also lead you to think about the issues in a way that helps you to identify the requirements of your own applications. Here's what we'll cover in the chapter:

  • An overview of font families, typefaces, fonts, and text.

  • How to specify coordinates when drawing text.

  • Text formatting (including horizontal and vertical centering, wrapping, vertical text, tab stops, and styles like strike-through and underline).

  • Drawing text using a GDI+ brush.

  • The metrics of fonts and text, including height and width.

  • How to control the quality of the text. We'll include discussion of the tradeoff between text quality and drawing speed. We'll also review aliasing and anti-aliasing as they relate to text-rendering, and study the two methods of anti-aliasing that exist in GDI+.

  • Specifying font sizes in the page coordinate system.

We've already seen one or two examples of text rendering in passing, in the earlier chapters of this book. This chapter serves not only to consolidate those examples, but also to provide a much more complete treatment of the issues surrounding the art of text rendering in GDI+.

Before we get into the various examples and practical points of this chapter, we'll start by setting out some basics about text and how GDI+ handles it.


An Overview of Text and Fonts

When we talk about a text string, we're really talking about a well-ordered string of characters, which is represented visually in some way in our drawing surface. In order to achieve this, there are a number of simple concepts involved-so let's begin by identifying those concepts and how they relate to one another. This is fairly straight forward, but it helps to set the scene for the chapter.

In order to store a text string, we need some system of characters that we can use to represent the string. The .NET Framework stores all text strings using the Unicode character encoding system. Unicode maps every alpha-numeric character, punctuation mark, and symbol (for every language) to a unique 16-bit (two byte) number.

Clearly, Unicode caters for a huge number of characters, and this is why a 16-bit representation is required. (Note that the ASCII character representation scheme is 7-bit, which allows 128 characters, and the Extended ASCII scheme is 8-bit, which allows only a 256 character scheme.) For this reason, some people refer to Unicode as widetext.

Prior to the advent of Unicode, there were hundreds of different encoding systems-there were several different encoding systems for European character sets alone. One problem with these systems was that different systems used different codes to represent the same character-this meant that any text string would require a system-to-system translation, unless the applications involved were using the same system. Another problem was that some of those old encoding systems that were designed to represent the characters of a particular language (such as English) didn't support a complete set of characters.

So, having a Unicode-enabled application helps for porting to European languages. In fact, it's also essential for far eastern languages such as Japanese and Chinese, which have much larger character sets.

Glyphs, Fonts, and Typefaces

The character encoding system itself helps us to distinguish the different characters in the character set, but it doesn't tell us what these characters look like. For that, we need glyphs and fonts. A glyph is a graphic symbol that provides the appearance or outline of a character. It is the graphical representation of an encoded character. (The word glyph is derived from the Greek word for carving.)

A font is a mapping between a set of (Unicode) character codes and a set of glyphs. Thus, a font describes the appearance of each of the characters in the set. Thus, we describe a text string by a sequence of (Unicode) character-codes, and we describe the visual appearance of that text string applying the font to each of the character codes.

It is important to note that Unicode characters are not glyphs. They only provide an abstract encoding of characters and symbols in a language. They have an expected appearance, but no explicit appearance. We require a font, which contains a description of the outline or physical representation of each character, to provide a set of glyphs that we can then paint onto our drawing surface.

TrueType Fonts and Raster Fonts

Most of the fonts that we use in Windows today are TrueType fonts, which follow the model we've described above. The outlines of the glyphs of TrueType fonts are described mathematically, and therefore can scale to any size.

A few years ago, raster fonts (sometimes called bitmap fonts), were also popular. A raster font consisted of a raster-based representation of each character, for a few predetermined point sizes. If we wanted to use a raster font to draw a text string but we wanted to draw it in a point size other than one supplied by the font, then the rendering engine would attempt to interpolate a representation from the predefined point sizes-and the results were often rather unsightly.

In the past, the computations required to take a mathematical description of an outline of a glyph and render the glyph on a raster-based display were too intensive for the available processing power-and therefore, for reasons of performance, we tended to use raster fonts. Today, however, our processors are more than sufficiently powerful to handle such processing-so TrueType fonts are being used more commonly and raster fonts will go the way of punch cards.

A Summary of Terminology

To complete this section, let's define four useful terms and their relationship to one another:

  • A glyph is a description of the outline of a particular letter, numeral, or other symbol in a given font. The outline of the letter ‘E', in Times-Roman-Bold-10Point, is an example of a glyph.

  • A font is a mapping of a set of characters onto a set of glyphs, which hence describes what the characters should look like. Times-Roman-Bold-10Point is an example of a font.

  • A typeface is a set of fonts, each of which looks the same but has a different size. Times-Roman-Bold is an example of a typeface.

    Often, in word processors, we will also see underline and strikethrough in the same dialog box where we specify the style of the font. However, we don't need separate typefaces to implement effects like underline and strikethrough; the rendering engine draws the underline or strikethrough after drawing the text. No new typeface is necessary.

  • A font family is a group of typefaces that have a similar basic design, with certain variations in style. The variations in style from the normal are typically bold, italic, and bold-italic. Times-Roman is an example of a font family.

Thus, a typeface is an instance of a font family with a particular style; and a font is an instance of a typeface for a given size; and a glyph is an instance of a font for a given character.

Working with Fonts in GDI+

With this understanding of the basic concepts involved in text rendering, we can now start to look at how it all works in GDI+. We can start to understand that by looking at GDI+'s FontFamily and Font classes, which both belong to the System.Drawing namespace.

The FontFamily Class

In GDI+, we use an instance of the FontFamily class to represent a font family-a group of typefaces that have a similar basic design with certain variations in styles. As we mentioned above, the variations in style typically extend to bold, italic, and bold-italic only. For example, the Arial font family contains four typefaces:

  • Arial Regular

  • Arial Bold

  • Arial Italic

  • Arial Bold Italic

Creating a FontFamily Object

There are a couple of different ways that we can construct a FontFamily object. For example, we can specify the name of the font family as a string, like this:

FontFamily ff = new FontFamily("Arial");

Alternatively, the FontFamily class provides a second constructor that expects a value from the GenericFontFamilies enumeration (which is part of the System.Drawing.Text namespace):

FontFamily ff = new FontFamily(GenericFontFamilies.Serif);

When we use this second form of the constructor, we don't need to specify a font family by name. Rather, we tell the constructor what type of font family we want, and it then supplies one that meets our requirements. This enumeration lists three basic types of font:

Value | Description
Monospace | A monospace font family is one where all characters are the same width. Courier is an example of a monospace font family. This type of font is often used for program listings.
Serif | A serif is a small decorative line that is an embellishment or decoration added to the basic form of a character. Times Roman is a font family that has serifs.
SansSerif | These font families do not have serifs. Examples include Helvetica and Arial.

Of course, if we use this constructor then we lose some control over exactly which font is used. When designing professional controls, we may want to be more specific about the exact font family to use-in which case, the first constructor is a more appropriate choice.

If we attempt to use a font that is not installed on the user's system, then Windows will pick another font. If we want to make sure that we are using a particular font, then we should install the font with the application, and we need to verify that the font is installed before we use it-we can use the InstalledFontCollection object to do this, as we'll see later in this chapter.

The Font Class

Once we have created our FontFamily object, we can use it to define a specific Font object:

FontFamily ff = new FontFamily("Arial");
Font f = new Font(ff, 12);

Alternatively, we can define our font directly, by specifying the name of the font family in the first argument of the constructor:

Font f = new Font("Arial", 12);

We can use this constructor if we don't have (or don't want to create) a FontFamily object.

It's worth noting that there's certain functionality that is only available via the FontFamily class. For example, if we need to know the sizes of detailed metrics for the font (such as the ascent and descent-the distance that the characters extend above and below the line). If you don't need access to these values, then the second constructor should do just fine. We'll return to the subject of font metrics later in this chapter.

Overloads for the Font Class Constructor

In fact, GDI+ provides us with quite a number of different overloaded constructors for the Font class. As we've seen, we can use a constructor that accepts a FontFamily object, or the name of the font family as a string (though not both); in either case, we also need to pass in the desired size of our font.

Whichever of these options we choose, GDI+ also gives us the option to describe the required font further if we wish. Moreover, there is one further constructor that allows us to create a Font object from another Font object, altering only the style. Here's a full list of the Font class constructors offered:

// Create a font from another font
public Font(Font, FontStyle);

// Define the font via the FontFamily object
public Font(FontFamily, Size);
public Font(FontFamily, Size, FontStyle);
public Font(FontFamily, Size, GraphicsUnit);
public Font(FontFamily, Size, FontStyle, GraphicsUnit);
public Font(FontFamily, Size, FontStyle, GraphicsUnit, GdiCharSet);
public Font(FontFamily, Size, FontStyle, GraphicsUnit, GdiCharSet,
                                                         VerticalFont);

// Define the font by passing the name of the font family in a string
public Font(FamilyName, Size);
public Font(FamilyName, Size, FontStyle);
public Font(FamilyName, Size, GraphicsUnit);
public Font(FamilyName, Size, FontStyle, GraphicsUnit);
public Font(FamilyName, Size, FontStyle, GraphicsUnit, GdiCharSet);
public Font(FamilyName, Size, FontStyle, GraphicsUnit, GdiCharSet,
                                                         VerticalFont);

Here's a summary of the arguments used in the Font constructors above:

  • The Font argument, used in the first of the above constructors, allows us to easily create a new font that is a variation of an existing font.

  • The Size argument is an integer that gives the em size of the constructed font, in terms of the GraphicsUnit. (A font's em size is a measurement that is proportional to its height. We'll meet the em size again later in the chapter.)

  • The FontFamily argument, as we've seen, is an existing FontFamily object (and hence specifies the font family of the desired font).

  • The FamilyName argument is a string that contains the name of the font family.

  • The FontStyle argument allows us to specify one of several styles for the font. Possible values for this argument are Bold, Italic, Regular, Strikeout, and Underline (these are the values of the FontStyle enumeration, which is part of the System.Drawing namespace).

    We can use the OR operator for this argument, to create a combination style such as bold-italic.

  • The GraphicsUnit argument specifies the unit of measurement used for the Size argument. The default GraphicsUnit for fonts is Point (1/72 inch). Other values are Display (1/96 inch), Document (1/300 inch), Inch, Millimeter, and Pixel (these are the values of the GraphicsUnit enumeration, which is part of the System.Drawing namespace).

Note that setting the GraphicsUnit for a font is different from setting the PageUnit for a Graphics object-although both are of type GraphicsUnit. The former controls the units with which we specify the size of the font; the latter controls the units of the position arguments of drawing operations.

  • The GdiCharSet argument is a byte that specifies a GDI character set to use for this font. When using GDI (instead of GDI+), we can use one of a number of character sets, including ANSI_CHARSET, GREEK_CHARSET, etc. The purpose of this option is backwards compatibility. If we are only using Unicode in our applications, we will not need to use this option.

  • The VerticalFont argument is a Boolean argument that indicates whether the new Font object is derived from a GDI vertical font. A vertical font is one that allows us to draw vertical text (that is, text which lies along a vertical line). We'll demonstrate drawing with vertical fonts later in the chapter.

As you can see, there are many Font constructors; but it should be quite easy just to pick the one that suits our requirements.

Disposing of Fonts

Finally on the subject of creating Font objects, it's worth noting that the Font class-like many GDI+ classes-implements the IDisposable interface, which includes the Dispose method. It is important to call the Dispose method when we are completely finished with a Font object (or to wrap the use of the Font in a using block so that it is disposed at the end of the specified scope).

The usual rule applies: if we create it, then we must destroy it.

Using the Default Font

When we draw to a drawing surface on the screen (such as a Form object), there is a default Font object available for our use. We don't need to create it or dispose of it-we get it from the Font property of the form. The following code demonstrates the use of this default Font object:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.DrawString("Hello World', this.Font, Brushes.Black, 0, 0);
}

The DrawString method call above expects a Font object to be specified in its second argument. Here, we've specified the Font property of the form itself, using the expression this.Font. The font is the default font for the window and will be the same font as that used for a button (unless we explicitly change the font for the button).

When building custom controls, there are three good reasons for using the Form object's Font property, rather than creating your own Font object, if possible. First, it is convenient: we don't need to create or dispose of this Font object. Second, text written in this font will match text for buttons, textboxes, radio buttons, etc. Third, when a localized application executes, the Font property will be set to a font that is appropriate to the local user interface culture.

Drawing Basic Text

The easiest way to draw text onto our drawing surface is to use the DrawString method of the Graphics object-which draws the text to a specific location on the drawing surface. We've just seen an example of this in the 'Hello World' example, above. In this case, we specified the location of the text by supplying X and Y coordinates, so that the text is placed at the position (0, 0) on the surface:

g.DrawString("Hello World", this.Font, Brushes.Black, 0, 0);

Drawing Text in a Bounding Rectangle

Another overload of the DrawString method takes a RectangleF structure as an argument, and uses it as the bounding box for the text that we want to draw:

Graphics g = e.Graphics;
RectangleF drawRect = new RectangleF(150.0f, 150.0f, 200.0f, 50.0f);
g.DrawString("Hello World", this.Font, Brushes.Black, drawRect);
The Height and Width of the Bounding Rectangle

Note that when we define the height of this rectangle, we need to be sensitive to the Height property of the font (and in particular, the units used to express the font height and the rectangle height)-so that the text will fit into the rectangle. In the example above, the rectangle height is 200 pixels. The rectangle size is expressed in terms of the PageUnit of the Graphics class, and the Font object's Height property is given in terms of the current graphics unit.

The default setting for both of these units is the pixel, and this makes it easy to compare the relative heights of the font and the rectangle. If you change one of these units (for example, by setting the Graphics.PageUnit property to GraphicsUnit.Inch), then be aware that the comparison becomes much more tricky.

Much easier is to use the MeasureString method to calculate both the height and width of a given text string rendered in a given font. This method returns a Size object, whose dimensions are given in terms of the current graphics unit. We'll see an example in a moment.

In fact, the pixel width of the string that we want to draw is a function of three parameters: the text string, the font, and the drawing surface. By default, when measuring the length of a string, GDI+ adds an extra length of 1/6 em-it does this to allow space for overhanging glyphs, such as the italic letter f. In addition, the width-calculating algorithm also adds space for something called grid fitting. This allows for the slight change in width that occurs, when we take the mathematical description of the outline of a glyph and map it onto a raster-based drawing surface.

Many times, when measuring strings, we don't want to have GDI+ add this extra length. We can instruct GDI+ to handle text in special ways using the StringFormat class. We can tell GDI+ to refrain from adding the additional space of 1/6 em by using a special StringFormat object, which we get by using the StringFormat.GenericTypography static property.

An Example Using Height and Width

Let's take a look at an example:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    FontFamily ff = new FontFamily("Times New Roman");
    Font f = new Font(ff, 12);
    String s = "Height: " + f.Height;
    SizeF sf = g.MeasureString(s, f, Int32.MaxValue,
                               StringFormat.GenericTypographic);
    RectangleF r = new RectangleF(0, 0, sf.Width, f.Height);

    g.DrawRectangle(Pens.Black, r.Left, r.Top, r.Width, r.Height);
    g.DrawString(s, f, Brushes.Black, r, StringFormat.GenericTypographic);

    f.Dispose();
}

The above code obtains the height of the text to be displayed from the Height property of the Font object. It also obtains height and width of the containing rectangle for the string, which returned as a SizeF object.

Then it creates and draws a rectangle whose width is given by the width of the bounding SizeF object (sf.Width), and whose height is given by the font's height, f.Height. Then we use the DrawString method to draw the text string s using the font f within the bounding rectangle r.

When you run the example, you should see the following:

Click To expand

The rectangle is 19 pixels high which means that, using the default coordinate system, GDI+ will draw 20 pixels in the vertical direction. The upper left corner of the rectangle is located at coordinate (0, 0) on the drawing surface.

Not all graphical toolkits specify location via the upper left corner of the rectangle. Some, including GDI (though not GDI+), specify a point at the left end of the baseline instead. (The baseline of a font is the line that runs under the bottom of the majority of the characters.) Thus, GDI+ represents a deviation from the past in this regard.

Drawing Text that Runs from Right-to-Left

The technique demonstrated above is one that works well in many cases. However, we need to beware if we're going to assemble many lines of text and use multiple calls to the DrawString method, in the case where we are trying to display text in Arabic, Hebrew, and in other languages that involve strings that are drawn from right to left.

The rules for drawing strings in languages that contain right-to-left formatting (or mixed right-to-left and left-to-right formatting) are defined by Unicode, and are rather complicated. The good news is that you don't need to worry about these rules, because you can let GDI+ handle this whole issue automatically. If you're dealing with right-to-left formatted text strings, then the best approach is to assemble the entire Unicode string, and then draw the string with a single call to the DrawString method.


Formatting Text

We've seen how to draw a short and simple phrase onto our drawing surface. Of course, there are many other simple but common effects that we want to achieve. For example, we might want to:

  • Change the appearance of the characters themselves (either selecting between normal, bold, italic, and bold-italic, or by adorning the characters with effects such as underlining or strikethrough)

  • Draw longer strings of text, and control the way that such strings wrap

  • Control positioning characteristics of the text-for example, by centering the text horizontally or vertically within a rectangle

  • Drawing vertically oriented text (which is particularly useful for labels on tables, graphs, and bar-charts)

  • Use tabbing techniques to create ‘columns' of text

  • Create elaborate effects by using brushes to draw text

In this section we'll see how GDI+ makes these features available for us. One of the main tools that we have to control the formatting and appearance of text is the StringFormat class. We'll look at some of the typical uses of this class.

Font Styles

The simplest way to format text, and probably the one that you will use most often, is simply to set the font style when you create the Font object. There are five basic options for font style: Bold, Italic, Regular, Strikeout, and Underline. We can use a Boolean OR (or |) to combine these values together and get combinations of style, such as bold-italic or bold-strikeout.

Here's a simple example that demonstrates this basic idea:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create some Font objects
    Font font   = new Font("Times New Roman", 12, FontStyle.Regular);
    Font bfont  = new Font("Times New Roman", 12, FontStyle.Bold);
    Font ifont  = new Font("Times New Roman", 12, FontStyle.Italic);
    Font bifont = new Font("Times New Roman", 12,
                                 FontStyle.Bold | FontStyle.Italic);
    Font sfont  = new Font("Times New Roman", 12, FontStyle.Strikeout);
    Font ufont  = new Font("Times New Roman", 12, FontStyle.Underline);
    Font bsfont = new Font("Times New Roman", 12,
                                 FontStyle.Bold | FontStyle.Strikeout);
    int h = font.Height;

    // Use each Font object to draw a line of text
    g.DrawString("Regular",          font, Brushes.Black, 0, 0);
    g.DrawString("Bold",             bfont, Brushes.Black, 0, h);
    g.DrawString("Italic",           ifont, Brushes.Black, 0, h*2);
    g.DrawString("Bold-Italic",      bifont, Brushes.Black, 0, h*3);
    g.DrawString("Strikeout",        sfont, Brushes.Black, 0, h*4);
    g.DrawString("Underline",        ufont, Brushes.Black, 0, h*5);
    g.DrawString("Bold & Strikeout", bsfont, Brushes.Black, 0, h*6);

    // Finally, dispose of all the Font objects
    font.Dispose();
    bfont.Dispose();
    ifont.Dispose();
    bifont.Dispose();
    sfont.Dispose();
    ufont.Dispose();
    bsfont.Dispose();
}

As you can see, the example just creates seven Font objects (each one representing the same font, but using a different style)-and then uses each font to write one line of text. We've used the FontStyle enumeration (part of the System.Drawing namespace) to achieve this. When the example is run, the window looks like this:

Click To expand

Lines, Alignment, and Orientation

Now let's move on from the subject of what the individual characters look like, to the way that they're laid out on the page. We want to be able to control the number of lines used to draw the text, or the width of each of those lines, or things like centering and turning the text on its side. We'll look at all those issues in this section, and show some illustrative examples of how GDI+ deals with them.

Multi-line Text

In some other toolkits, the process of drawing multi-line text is a rather complicated one-first calculating the pixel widths of different substrings to determine where to break the line between words, and then drawing each successive substring, each at exactly the correct location. In GDI+, none of this is necessary. We can easily draw multi-line text using word wrapping. All we need to do is call the DrawString method, passing a rectangle whose width is less than the width of the text string, and whose height is enough to house the multiple lines of text:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Declare a long text string
    String s = "This string is long enough to wrap. ";
    s += "With a 200px-width rectangle, and a 12pt font, ";
    s += "it requires six lines to display the string in its entirety.";

    // Declare a Font object and a Rectangle object
    Font f = new Font("Arial', 12);
    Rectangle r = new Rectangle(20, 20, 200, f.Height*6);

    // Draw the rectangle.
    // Also draw the string, using the rectangle as a bounding box
    g.DrawRectangle(Pens.Black, r);
    g.DrawString(s, f, Brushes.Black, r);

    f.Dispose();
}

The result of this code looks like this:

Click To expand
Controlling the Number of Lines at Run Time

In the example above, we used a rectangle of width 200 pixels, and we could experiment to check that this rectangle was wide enough to contain the whole string in 6 lines. But what can we do if, at design time, we don't know the text string that is to be drawn? In that case, we certainly don't know the length of the string, and therefore we don't know whether the rectangle r will be big enough.

In that case, we can use the MeasureString method to calculate a Size object whose dimensions reflect the length of the string and any width restriction-and then use the Size object to set the width and height of the rectangle. In the following example, we'll use this technique to draw some text into a rectangle that must be no more than 150 pixels wide, but can contain as many lines as necessary:

// Declare a long text string
String s = "This string is long enough to wrap. ";
s += "We'll use a 12pt font, and assume ";
s += "the text string must fit into a width of 150 pixels. ";

// Declare a Font object and a Rectangle object
Font f = new Font("Arial", 12);
SizeF sf = g.MeasureString(s, f, 150);
RectangleF rf = new RectangleF(20, 20, sf.Width, sf.Height);

// Draw the rectangle.
// Also draw the string, using the rectangle as a bounding box
g.DrawRectangle(Pens.Black, rf.Left, rf.Top, rf.Width, rf.Height);
g.DrawString(s, f, Brushes.Black, rf);

In this example, GDI+ calculates at run time that the string requires seven lines.

Click To expand
Explicit Line Breaks and Forcing NoWrap

If we wish, we can explicitly control where a text string will break, by inserting new line (\n) characters into the text in the appropriate places. For example, suppose we modified the above code as follows:

String s = "This string is long enough\n to wrap. ";

Now, the code will force a line break after the word enough.

By contrast, we can prevent wrapping of text by creating a StringFormat object, setting the FormatFlags property to StringFormatFlags.NoWrap, and passing the StringFormat object to the DrawString method, as follows:

// Declare a long text string
String s = "This string is long enough to wrap. ";

// Declare a Font object and a Rectangle object
Font f = new Font("Arial", 12);
Rectangle r = new Rectangle(20, 20, 150, f.Height*4);

// Declare the StringFormat object, and set the FormatFlags property
StringFormat sf = new StringFormat();
sf.FormatFlags = StringFormatFlags.NoWrap;

// Draw the rectangle.
// Also draw the string, using the rectangle as a bounding box
g.DrawRectangle(Pens.Black, r);
g.DrawString(s, f, Brushes.Black, r, sf);
f.Dispose();

The value StringFormatFlags.NoWrap belongs to the StringFormatFlags enumeration, which is part of the System.Drawing namespace. When we run this code, the text doesn't wrap but instead disappears under the right-hand edge of the rectangle, like this.

Click To expand

Horizontally and Vertically Centered Text

In the examples above, the text is (by default) horizontally aligned to be flush with left edge of the containing rectangle. But of course, we may not always want this. We may want a different horizontal alignment-center-aligned or flush with the right edge.

In fact, in those examples, the text is also (by default) vertically aligned to be flush with the top edge. This isn't so obvious, because in all of those examples the containing rectangle was only just large enough to completely contain the text. But given a bit more space in our rectangle, we may also want to align the text vertically-centered, or close to the bottom edge.

To do this, we can once again take advantage of the features of the StringFormat object. This time, we set its Alignment and LineAlignment properties to the appropriate values, then pass it to the DrawString method.

In the following example, we'll place the text in a nice big rectangle with lots of extra whitespace. We'll center-align the text horizontally and vertically, using the Alignment and LineAlignment properties of a StringFormat object:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create a text string, a Font object, and a StringFormat object
    String s = "This is a long string that will wrap. ";
    s += "It will be centered both vertically and horizontally.";
    Font f = new Font("Arial", 12);
    StringFormat sf = new StringFormat();

    // Center each line of text horizontally and vertically
    sf.Alignment = StringAlignment.Center;        // horizontal alignment
    sf.LineAlignment = StringAlignment.Center;    // vertical alignment

    // Draw a rectangle and the text string
    Rectangle r = new Rectangle(0, 0, 300, f.Height*4);
    g.DrawRectangle(Pens.Black, r);
    g.DrawString(s, f, Brushes.Black, r, sf);
    f.Dispose();
}

The text is formatted as follows:

Click To expand

Drawing Vertical Text

There are two ways to draw vertically oriented text. One way is to define some horizontally oriented text (such as the text that we've seen in any of the examples above), and then transform the coordinate space with a rotation through 90°. We won't cover this technique here-in fact, we will examine all sorts of different transformations (including rotations) in .

However, there is a second (and much easier) way to draw vertical text. Namely, we can create a StringFormat object specifically to draw vertical text. One obvious advantage of this technique over the transformation technique is that we don't change the orientation of the entire coordinate system!

To do this, we use an overload of the StringFormat class constructor, passing the value DirectionVertical (from the StringFormatFlags enumeration, which belongs to the System.Drawing namespace). The following code demonstrates vertical text drawing in this manner:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create a text string, a Font object, and a StringFormat object
    String s = "Accrington Stanley";
    StringFormat sf = new StringFormat(StringFormatFlags. DirectionVertical);
    Font f = new Font("Times New Roman", 14);

    // Calculate the size of the text string's containing rectangle
    SizeF sizef = g.MeasureString(s, f, Int32.MaxValue, sf);

    // Create and draw the rectangle
    // Also draw the text string (using the StringFormat object)
    RectangleF rf = new RectangleF(20, 20, sizef.Width, sizef.Height);
    g.DrawRectangle(Pens.Black, rf.Left, rf.Top, rf.Width, rf.Height);
    g.DrawString (s, f, Brushes.Black, rf, sf);

    f.Dispose();
}

In this code, note that we call a version of the MeasureString method that expects a StringFormat object passed as the fourth argument. The dimensions of the resulting Size object take account of the fact that we plan to orient the text vertically-in this case, the Size object is much taller than it is wide. Thus, we can use those dimensions, sizef.Width and sizef.Height, explicitly to set the width and height of the rectangle in which the text string is drawn.

Click To expand

While this technique for drawing vertical text is easier than applying a rotation transformation, it's also rather less flexible. If we use a rotation transformation, we have the freedom to draw the text at any angle, while the technique shown above allows us only to orient our text vertically downwards (a 90° clockwise rotation). We'll look at transformations in Chapter 8.

Tab Stops

One particularly nice feature of GDI+'s text string formatting system is the way it implements tab stops. If we want to use tab stops, we first need to set them up-and we do this using the StringFormat object's SetTabStops method. Once we've done that, we can use the tab stops in the text that we want to draw, by including the tab character (\t) in the text string itself and passing the text string and the StringFormat object to the Graphics.DrawString method.

Let's take a closer look. In the following example, we'll use a set of four tab stops to create the effect of a table with four columns:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.FillRectangle(Brushes.White, this.ClientRectangle);

        // Create a couple of Font objects
        Font f = new Font("Times New Roman", 12);
        Font bf = new Font(f, FontStyle.Bold);

        // Create a StringFormat object, and set the tab stops, in pixels
        StringFormat sf = new StringFormat();
        float[] ts = {10.0f, 70.0f, 100.0f, 90.0f};
        sf.SetTabStops(0.0f, ts);

        // Create some text strings.
        // The \t escape-sequence in the these lines specifies the tab
        string s1 = "\tName\tHair Color\tEye Color\tHeight";
        string s2 = "\tBob\tBrown\tBrown\t175cm";

        // Use the text string, Font, StringFormat etc
        //to draw the text strings
        g.DrawString (s1, bf, Brushes.Black, 20, 20, sf);
        g.DrawString (s2, f, Brushes.Blue, 20, 20 + bf.Height, sf);

        f.Dispose();
        bf.Dispose();
    }

In the first few highlighted lines here, we create a StringFormat object and an array of four float values, and we assign the array to the StringFormat object's SetTabStops property. The array represents the number of tab stops that we want to set, and the width of each. So, the first tab stop will be 10 pixels from the left of the containing rectangle. The second will be 70 pixels further along; the third will by 100 pixels after that, and the fourth is 90 pixels after that.

Hence, the position of the first tab is relative to the left-hand edge of the rectangle, and every subsequent tab stop is positioned relative to the previous tab. Note that, in this example, the tab stop widths specified will be measured in pixels. GDI+ uses whatever unit is set for the PageUnit property of the Graphics object-in this case, it's the default, which is the pixel. We'll meet the the PageUnit property properly, later in this chapter.

With that done, the tab stop positions are set-all we have to do is use them. To do that, we create some lines of text that include \t escape sequences, and draw them (using the Graphics.DrawString method and specifying our StringFormat object). The result is as shown below:

Click To expand

In this example, our first tab is a 10-pixel tab, and we've placed a \t at the start of each text string. We don't have to do this, but its effect is to create an ‘empty 10-pixel column' at the left end of the table.

Mixing \t and \n

We can also put new line (\n) characters into the text, and hence draw multiple lines of text that are separated by tab stops:

// Create some text strings.
// The \t escape-sequence in the these lines specifies the tab
string s1 = "\tName\tHair Color\tEye Color\tHeight";
string s2 = "\tBob\tBrown\tBrown\t175cm";
string s3 =
      "\tMary\tBlond\tHazel\t161cm\n\tBill\tBlack\tBlue\t168cm";

// Use the text string, Font, StringFormat etc
// to draw the text strings
g.DrawString(s1, bf, Brushes.Black, 20, 20, sf);
g.DrawString(s2, f, Brushes.Blue, 20, 20 + bf.Height, sf);
g.DrawString(s3, f, Brushes.Blue, 20,
                                  20 + bf.Height + f.Height, sf);

This gives the following results:

Click To expand
Right-justified Tab Stops

There are no facilities for creating centered or right-justified tab stops. However, if we use a fixed width (monospace) font, we can simulate right-justified tabs by using the String.Format method to right-justify text within the string that we draw using the DrawString method. In this example, we do just that:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Font f = new Font("Courier New", 12);
    Font bf = new Font(f, FontStyle.Bold);

    // create a StringFormat object, and set the tab stops, in pixels
    StringFormat sf = new StringFormat();
    float[] ts = { 100.0f, 100.0f, 90.0f };
    sf. SetTabStops(0.0f, ts);

    // Create a string that describes how the specified values
    // should be formatted
    string fs = "{0}\t{1,8}\t{2,8}\t{3,8}";

    // Create some formatted text strings
    string s1 = String.Format(fs, "Month", "Revenue", "Expense", "Profit");
    string s2 = String.Format(fs, "January",
                                         "900.00", "1050.00", "−150.00");
    string s3 = String.Format(fs, "February",
                                         "1100.00", "990.00", "110.00");

    // Draw the text strings
    g.DrawString(s1, bf, Brushes.Black, 20, 20, sf);
    g.DrawString(s2, f, Brushes.Blue, 20, 20 + bf.Height, sf);
    g.DrawString(s3, f, Brushes.Blue, 20, 20 + bf.Height + f.Height, sf);

    f.Dispose();
    bf.Dispose();
}

The monospace font we've chosen here is Courier New. We also set three tab stops (of widths 100, 100, and 90 pixels). Then we create a ‘template' formatting string, fs, which describes how each of the strings is to be formatted. The format string has the value {0}\t{1,8}\t{2,8}\t{3,8}. This looks complicated but it can be interpreted easily. Each of the expressions in {}-brackets represents a value. The \ts indicate where we use the tab stops. So, each text string that we draw will consist of four values separated by three tab stops.

So, what are the values? We specify them in the strings that we define in the subsequent lines, using the String.Format method. This method expects a formatting string and a list of values-the values are indexed using a 0-based index (0, 1, 2, 3 in this case). When we draw the string, we place the ‘0th' value in place of {0}, the ‘1st' value in place of {1, 8}, etc., to get something like this:

Month\tRevenue\tExpense\tProfit

There's just one thing left. The second value in the {}-bracket expressions, if it exists, is an integer that indicates whether the value should be left- or right-justified, and the character-width of the value. The value will be padded out with spaces if it's too small. If the value is positive, then it will be right-justified; if it's negative or absent, it'll be left-justified. In this case, we get one left-justified column, and three right-justified columns that are 8 characters in width:

Month      \t Revenue\t Expense\t  Profit

Ultimately, the result is this:

Click To expand

If we wanted to achieve this type of alignment using a non-monospace font, we would need to calculate positions of the substrings of our text and draw the substrings ourselves. Alternatively, we could use a DataGrid, or some other more advanced control.

Displaying Text using a Brush

So far, we have only seen examples that use a black brush to draw text strings. However, we can use any type of (custom or prebuilt) Brush object, including any of the Brush objects we described in . Consider the following example:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Font f = new Font("Times New Roman", 48, FontStyle.Bold);
    HatchBrush hb = new HatchBrush(HatchStyle.Cross,
                                              Color.White, Color.Black);
    g.DrawString("Crazy Crosshatch", f, hb, 0, 0);
    f.Dispose();
}

This example creates a HatchBrush, and then draws text using that brush. (If you try it, don't forget that the HatchBrush class is in the System.Drawing.Drawing2D namespace.) When run, the output looks like this:

Click To expand


Dealing with Fonts

In this last section of this chapter, we'll take a look at a number of issues that relate specifically to fonts and font-handling. We'll find out how to check that a desired font has been installed onto a system. We'll also take a look at the subject of font metrics-the different measurements that are used in the description of a font.

In addition, we'll revisit the issue of aliasing and anti-aliasing, and see specifically how it relates to the quality of rendered text in GDI+.

Finding Available Fonts

It's sometimes useful to be able to find out what fonts are installed on the local machine. In fact, this is quite an important ability if your application is going to be installed on different computers, unless fonts are included as part of the installation.

We can check for available fonts by using the InstalledFontCollection class (which is part of the System.Drawing.Text namespace). The following code gets a list of all of the installed font families and writes the list to the console window:

FontFamily[] fontFamilies;
InstalledFontCollection installedFontCollection =
                                   new InstalledFontCollection();

fontFamilies = installedFontCollection.Families;

for (int i = 0; i < fontFamilies.Length; ++i)
    Console.WriteLine("FontFamily name: " + fontFamilies[i].Name);

This example writes output to the console window. In your application's Properties, you need to ensure the Output Type is set to Console Application .

Here's a portion of the output that I get in the console window when I run this on my own machine:

FontFamily name: Arial
FontFamily name: Arial Black
FontFamily name: Arial Narrow
FontFamily name: AvantGarde Bk BT
FontFamily name: AvantGarde Md BT
FontFamily name: Book Antiqua
FontFamily name: Bookman Old Style
FontFamily name: Century Gothic
...

Here's another technique that we can use to get a list of font families. We can call the GetFamilies method of the FontFamily class (passing an instance of the Graphics class as an argument to the method), like this:

Graphics g = e.Graphics;
FontFamily[] families = FontFamily.GetFamilies(g);

foreach (FontFamily family in families)
    Console.WriteLine("FontFamily name:" + family.Name);

This code will provide us with a list of font families that are available for the specified graphics context. The output of this code is identical to that of the previous example.

Now let's move on to look at metrics of fonts, and the component pieces of fonts. This is a subject that will help us to gain a more complete control over where we write text on our drawing surfaces.

Font Metrics and Precision Text Placement

In a discussion of how .NET handles font metrics, the first thing we need to know is the component pieces of a font. The four main metrics that we are concerned with are the ascent, descent, leading, and font height. The following diagram defines each of these metrics:

Click To expand

The term leading is pronounced 'ledding'. It is so-called because typographers used to use lead strips between lines of characters on a printing press. As you can see, it is used to refer to the line spacing-the empty space between consecutive lines.

Sometimes typographers use a different model, in which the ascent is broken down into the x height and the ascender, and the descent is referred to as the descender:

Click To expand

The first model is the one used by .NET, and the one that we will use in this book.

The em

To really understand how to measure text, there is one more metric that we need to cover-the em (or em height). Contrary to what we might think, the em height is not (necessarily) the height of the 'M' character. Rather, an em is a unit of measurement defined as the point size of the font. Thus, if we're using a 12 point font then the length of the em unit is 12 points.

The em is also used to define the height and width of something called the em square-this is a term used in typography to describe the grid on which the typographer designs the glyphs of a font. The em square is a square grid that measures 1 em by 1 em measured in design units. A design unit has no intrinsic relationship to any real-world measurement; in the case of TrueType fonts, there are typically 2048 design units in 1 em. The typographer lays out the glyphs on the grid using a variety of abstractions to describe the arcs and lines that make up the shape of the glyph.

Sometimes design units are called font units, em units, or grid units. In .NET, they are called design units.

In .NET, we need to use the em height to convert between design units and pixels when calculating the metrics of fonts, as we'll see next.

Design Units and Resolution

In GDI+ (although not in GDI), text can be resolution independent. This means that the forms and windows that we build using GDI+ can look the same at all resolutions. Design units are key to accomplishing this. As mentioned, design units are the smallest units that typographers use to design their fonts.

Sometimes when building custom controls, we are content to allow GDI+ do the placement of text. In these cases, such as in the examples we've seen so far in this chapter, we simply supply a bounding rectangle for the text-and GDI+ positions the text inside the rectangle. However, in other cases, we may want to position the text explicitly and precisely. For example, we may want the baseline of the text to line up visually with some other part of our control; or we may want to center the text vertically and precisely relative to a horizontal line. In either of these cases, we will need to get detailed metrics of the font.

Getting the Font Metrics of a Font Family

To demonstrate this part, let's look at an example in which we ask GDI+ to tell us various different font metrics-ascent, descent, em height, and linespacing (leading)-for three different fonts. We can query the FontFamily class for these details, and use a format string (like the one we used earlier in this chapter) to display them to the console window:

// Create format string
String formatString = "{0,−16}{1,8}{2,9}(3,10}{4,14}";

// Write first line of table
Console.WriteLine(formatString, 'Font Family Name', 'Ascent', 'Descent',
                                "EmHeight", "Line Spacing");

 // Write font metrics for Courier New font family
FontFamily ff = new FontFamily("Courier New");
Console.WriteLine(formatString, ff.GetName(0),
    ff.GetCellAscent( FontStyle.Regular ),
    ff.GetCellDescent( FontStyle.Regular ),
    ff.GetEmHeight( FontStyle.Regular ),
    ff.GetLineSpacing( FontStyle.Regular ));

// Write font metrics for Arial font family
ff = new FontFamily("Arial");
Console.WriteLine(formatString, ff.GetName(0),
    ff.GetCellAscent( FontStyle.Regular ),
    ff.GetCellDescent( FontStyle.Regular ),
    ff.GetEmHeight( FontStyle.Regular ),
    ff.GetLineSpacing( FontStyle.Regular ));

// Write font metrics for Times New Roman font family
ff = new FontFamily("Times New Roman");
Console.WriteLine(formatString, ff.GetName(0),
    ff.GetCellAscent( FontStyle.Regular ),
    ff.GetCellDescent( FontStyle.Regular ),
    ff.GetEmHeight( FontStyle.Regular ),
    ff.GetLineSpacing( FontStyle.Regular ));

When we run this code, it will output the following:

Font Family Name Ascent Descent EmHeight Line Spacing
Courier New        1705     615     2048         2320
Aria1              1854     434     2048         2355
Times New Roman    1825     443     2048         2355

The values that we've retrieved from the FontFamily here are expressed in terms of DesignUnits. As you can see, each of these three TrueType fonts:

  • was designed using 2048 design units to 1 em

  • has an ascent of roughly ¾ em (though each is a little different)

  • has a descent of roughly ¼ em (though each is a little different)

  • has line spacing of a little over 1 em (though each is a little different)

Converting Font Metrics into Physical Measurements

The values that we've just retrieved are dependent only on the font family, and not on the font. So, for example, in terms of design units, the font metrics of 12pt Arial are the same as 26pt Arial. This isn't surprising, because different-sized instances of the same font family come from the same design, and therefore use the same glyphs that the typographer designed using the 2048-units-per-em grid.

If we want to use these values to achieve precise positioning of our text, then we need to convert them from DesignUnits to the current GraphicsUnit of our drawing surface, using the em size of the font family instance in which the text is to be drawn. This maps the relative sizes of the font metrics (given above) to actual measurements on our drawing surface.

In the next example, we're going to work out how to do that. Let's consider how we're going to do it. First, recall that when we construct a font, we pass the em size for the desired font as an argument to the Font constructor, like this:

Font f = new Font("Times New Roman", emSize);

By default, the desired em size is assumed to be in units of GraphicsUnit.Point. So, if we want a 12-point Arial font, we could use this:

Font f = new Font("Arial", 12);

This gives us an instance of the Arial font family whose em size is 12pt. As it happens, we're not obliged to use a Point measurement to specify the em size of the font-we can construct an instance of a font using any of the available GraphicsUnit members. Moreover, with any instantiated font, we can use the Font.Size property to get the em size of the font in terms of the GraphicsUnit for the font.

Now, since we know the em size of the font both in terms of the GraphicsUnit and in terms of the DesignUnits, we can easily convert from design units to GraphicsUnit using this formula:

Click To expand
An Example

We can demonstrate this in action with the following code. Remember the point of the exercise-to use these measurements to get precise alignment between the text string and other elements on the page. So in the following example, we'll draw a couple of lines of text and then place a few well-chosen lines at precise positions in relation to the test strings.

This is a longer example than previous ones, so we'll break it up a little. The first task of interest is to create a font family, and then a font instance (with a 24 point emSize):

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.FillRectangle(Brushes.White, this.ClientRectangle);

        // create the font family
        FontFamily ff = new FontFamily("Times New Roman");

        // create the font, passing 24f as the emSize argument
        float emSizeInGU = 24f;
        Font f = new Font(ff, emSizeInGU);

Now we have a font family and a font, we'll prepare for the design-unit-to-graphics-unit conversion. To do this, we need to get the values for the emSize, ascent, descent and line-spacing in terms of design units. Recall that these are the same for every font in the font family-so we get these values from the FontFamily object:

// get the design units metrics from the font family
int emSizeInDU = ff.GetEmHeight(FontStyle.Regular);
int ascentInDU = ff.GetCellAscent(FontStyle,Regular);
int descentInDU = ff.GetCellDescent(FontStyle,Regular);
int lineSpacingInDU = ff.GetLineSpacing(FontStyle.Regular);

Now we can use our conversion formula to convert the ascent, descent and line-spacing from design units to graphics units. This tells us the size of these three metrics, in points, for the 24pt instance of this font:

// calculate the GraphicsUnits metrics from the font
float ascentInGU      = ascentInDU*(emSizeInGU/emSizeInDU);
float descentInGU     = descentInDU*(emSizeInGU/emSizeInDU);
float lineSpacingInGU = lineSpacingInDU*(emSizeInGU/emSizeInDU);

For the sake of interest, we'll write these values to the console window (using the format string technique we covered earlier in this chapter):

// output the metrics to the console
Console.WriteLine("emSize = " + emSizeInDU + ' DesignUnits");
Console.WriteLine("emSize = " + emSizeInGU + " GraphicsUnits");
string format = "(0,−16}{1,12}{2,16}";
Console.WriteLine(format,
       "Font Metric",  "DesignUnits",   "GraphicsUnits");
Console.WriteLine(format,
       "Ascent",       ascentInDU,      ascentInGU);
Console.WriteLine(format,
       "Descent",      descentInDU,     descentInGU);
Console.WriteLine(format,
       "Line Spacing", lineSpacingInDU, lineSpacingInGU);

Now, the proof of the pudding is in the eating-so we'll draw some text strings and some lines and see how well they align. First, we'll draw two simple lines of text using our 24pt font. In the code below, the first two lines create two points that will form the top-left corners of the rectangles containing the two lines of text (they are a distance f.Height apart). The next two lines draw the text strings:

// draw two lines of text string
PointF textOrigin = new PointF(20, 20);
PointF nextLineOrigin = new PointF(textOrigin.X,
                                   textOrigin.Y + f.Height);

g.DrawString("AxgQ", f, Brushes.Black, textOrigin);
g.DrawString("AxgQ", f, Brushes.Black, nextLineOrigin);

Now let's try to use the results of our conversion to draw some lines that are precisely aligned to the text. Specifically, we'll draw horizontal lines to mark:

  • The top of the leading (the line of the origin)

  • The top of the ascent (the highest reach of the tallest characters)

  • The bottom of the ascent (the baseline)

  • The bottom of the descent (the lowest reach of the characters with tails)

First, here's the code for drawing the two lines at the top of the leading:

// draw a lines at the textOrigin
int lineLen = 100;
g.DrawLine(Pens.Blue,
      textOrigin,
      new PointF(textOrigin.X + lineLen, textOrigin.Y));
g.DrawLine(Pens.Red,
      nextLineOrigin,
      new PointF(nextLineOrigin.X + lineLen, nextLineOrigin.Y));

We've set the line-length to 100 pixels; the top lines will be blue and the bottom lines red. Each DrawLine method call draws a line between points, whose coordinates are calculated using the coordinates of the origin for the text string.

Now comes the real test: using lineSpacingInGU, ascentInGU, and descentInGU to draw similar lines for the other levels specified above. First the lines for the bottom of the ascent:

// draw a line at the baseline
PointF p = new PointF(textOrigin.X,
                      textOrigin.Y + lineSpacingInGU);
g.DrawLine(Pens.Blue, p,
                      new PointF(p.X + lineLen, p.Y));

p = new PointF (nextLineOrigin.X,
                nextLineOrigin.Y + lineSpacingInGU);
g.DrawLine(Pens.Red, p,
                     new PointF(p.X + lineLen, p.Y));

Now the lines for the top of the ascent:

// draw a line at the top of the ascent
p = new PointF(textOrigin.X,
              textOrigin.Y + lineSpacingInGU − ascentInGU);
g.DrawLine(Pens.Blue, p,
                      new PointF(p.X + lineLen, p.Y));

p = new PointF(nextLineOrigin.X, nextLineOrigin.Y +
     lineSpacingInGU − ascentInGU);
g.DrawLine(Pens.Red, p, new PointF(p.X + lineLen, p.Y));

And finally, the lines for the bottom of the descent:

    // draw a line at the bottom of the descent
    p = new PointF(textOrigin.X,
                   textOrigin.Y + lineSpacingInGU + descentInGU);
    g.DrawLine(Pens.Blue, p,
                          new PointF(p.X + lineLen, p.Y));

    p = new PointF(nextLineOrigin.X,
                   nextLineOrigin.Y + lineSpacingInGU + descentInGU);
    g.DrawLine(Pens. Red, p,
                          new PointF(p.X + lineLen, p.Y));
}

When we run this, we see the two lines of text drawn to the window. We can also see the two pairs of four horizontal lines that illustrate the various font metrics-precisely and correctly aligned to the text string itself. We were able to achieve this only when we took the design-unit values of the metrics from the FontFamily object, and converted them into the 'real' measurements that relate to this particular (24pt) font instance.

Click To expand

Finally, let's check the output that was written to the console window:

emSize = 2048 DesignUnits
emSize = 24 GraphicsUnits
Font Metric      DesignUnits GraphicsUnits
Ascent                  1825      21.38672
Descent                  443      5.191406
Line Spacing            2355      27.59766

It confirms that the emSize is 2048 design units, and that (in the case of this font) it's equal to 24 points. It also tells us the ascent, descent, and line-spacing for 24-point Times New Roman, in points.

Quality of Text

When we use GDI+ to draw text, we are given quite a bit of control over the visual quality of the text. To specify the desired level of quality, we can use the TextRenderingHint property of the Graphics class-this property can be set to any value from the TextRenderingHint enumeration (which is part of the System.Drawing.Text namespace).

There are three basic levels of text quality. If you read ahead a few pages (or check the documentation on the TextRenderingHint enumeration at http://msdn.microsoft.com/library) you'll see that there are five different values plus the ‘system default', but the five different options can be roughly approximated as follows:

  • No anti-aliasing

  • Anti-aliasing

  • Anti-aliasing using ClearType font features

Drawing without ClearType Font Features

Characters drawn without any anti-aliasing looks like this:

Click To expand

The standard way of anti-aliasing involves using different lighter shades to draw the pixels that are on the boundaries of the glyphs that make up the characters. This works best on CRT displays. The following diagram shows the same characters with standard anti-aliasing:

Click To expand

For a more complete discussion of the techniques of anti-aliasing, see Chapter 2. In this chapter, we're going to focus on ClearType font features.

Using ClearType Font Features

ClearType anti-aliasing is a technology that is patented by Microsoft and gives significantly clearer text than standard anti-aliasing. It only works on color LCD screens, and only in Windows XP. It uses the fact that each pixel on a color LCD screen is actually made up of three narrow vertical pixels-one each of the colors red, green, and blue. By setting colors at the edge of characters appropriately, ClearType anti-aliasing sets the brightness of these tall narrow pixels, creating a smoother appearance to the text.

While it is a little difficult to show the colored pixels in a monochrome illustration, the following is an image from a digital microscope that shows the vertical colored pixels that make up an LCD screen:

Click To expand

Unaided, the human eye cannot perceive these small individual pixels, and if all three pixels are fully on (that is, as bright as they can be), then the combined effect appears to be white.

A Comparison of Effects

The following three images shows how the same character is treated differently by the three levels of ‘quality' listed above. (If you want to compare the color versions of these images, you can download them-along with the source files for this book-from http://www.wrox.com.)

This image shows a lower case letter c without any anti-aliasing. As you can see, the narrow vertical pixels are turned on and off three at a time:

Click To expand

The next screen shot shows the same character using standard anti-aliasing. In this case, we can see that the standard anti-aliasing modifies the brightness of the narrow vertical pixels three at a time. When the standard anti-aliasing modifies three of these tall, narrow pixels, it is modifying the brightness of a single logical pixel on the screen:

Click To expand

The next screen shot shows anti-aliasing using ClearType. The anti-aliasing algorithm sets the brightness of the individual red, green, and blue pixels. It doesn't completely turn the narrow vertical pixels on or off; instead it sets the brightness of them to give the best appearance of the text:

Click To expand
Hardware Issues

ClearType is currently only implemented for screens whose left-to-right ordering of the sub-pixels is red-green-blue. This is a hardware issue. Most LCD screens are ordered red-green-blue, but some are not.

In addition, at the time of writing, ClearType anti-aliasing is not supported for text strings that are drawn in any direction other than horizontally. The reason for the current lack of support is simple: vertically-aligned, ClearType anti-aliased text would require a completely different algorithm from the one included in the current implementation. It is conceivable that Microsoft could implement ClearType anti-aliasing for vertically-aligned text (or indeed, for text aligned at any angle) in the future.

Quality versus Performance

My personal opinion is that ClearType anti-aliasing is very good. It gives a better appearance than text rendered using either no anti-aliasing, or standard anti-aliasing. In particular, when using ClearType anti-aliasing on a laptop that has a native resolution of approximately 120 DPI, aliasing artifacts are almost completely eliminated.

However, there is a noticeable speed trade off. On a Pentium II laptop that is running at 300 MHz, the drawing speed is visibly slower when using the ClearType anti-aliasing, although it is adequate. On a faster computer, such as a Pentium III 1 GHz laptop, GDI+ draws using ClearType so quickly that there is no noticeable drawing time. If we draw our text without explicitly setting the anti-aliasing option in our code, then text is drawn according to the advanced effects settings in the display properties. Users can then decide whether or not to enable ClearType anti-aliasing, and our text drawing code will conform to their preference.

Options for Text-rendering Quality

When drawing very large and very small text in a given font, it is not enough to just make the glyphs bigger or smaller. The widths of certain parts of the characters will look out of proportion if we simply resize them to extreme sizes. To solve this problem, the font description often contains hints that tell the rendering engine about how to change the widths of certain parts of the characters, so that the font looks good at very large sizes and very small sizes.

As we've mentioned, we set the level of text quality via the TextRenderingHint property of the Graphics object. Some of these values tell GDI+ to adjust stem widths as appropriate when drawing in much larger fonts. (The stems of a glyph are the principal strokes-so J has one stem and H has two.) It takes more CPU time to draw with stem-width hinting, but on today's fast computers, this is not really noticeable.

There are six values of the TextRenderingHint enum:

Value | Description
AntiAlias | Anti-aliased characters are drawn without stem-width hinting.
AntiAliasGridFit | Anti-aliased characters are drawn with stem-width hinting.
ClearTypeGridFit | ClearType anti-aliased characters are drawn with stem-width hinting. This is the slowest to process, but with the best appearance. There is no option to draw using ClearType while not using stem-width hinting.
SingleBitPerPixel | Characters that are not anti-aliased are drawn without stem-width hinting. This is the fastest to process, but with the lowest-quality appearance.
SingleBitPerPixelGridFit | Characters that are not anti-aliased are drawn with stem-width hinting.
SystemDefault | The system's default smoothing setting is used. For most purposes, it is recommended that you use this setting.

In Windows XP, users can specify the required level of anti-aliasing (none, standard, or ClearType) via the Display Properties dialog box. In the code for the control, if the TextRenderingHint property is set to SystemDefault then GDI+ draws according to preferences set in this box. However, we can override the user's preferences by explicitly setting TextRenderingHint to one of the other values.

In Windows 2000, in the Effects tab of the Display Properties dialog box, there is a checkbox that allows the user to specify that edges of screen fonts should be smoothed. However, this setting doesn't have any effect on text that we draw using GDI+. If we want to use standard anti-aliasing, we must set the TextRenderingHint property to AntiAlias. ClearType anti-aliasing is not available with Windows 2000.

The following lines of code give an illustration of text to be drawn in a Windows 2000 environment using standard anti-aliasing:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.TextRenderingHint = TextRenderingHint.AntiAlias;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    g.DrawString("Hello", this.Font, Brushes.Black, 0, 0);
}

Setting the GraphicsUnit for a Font

One of the main features of GDI+ is that it enables us to draw device-independent graphics. The most difficult problem to overcome when drawing device-independent graphics is the issue of dealing with a wide range of display resolutions. One of the tools at our disposal is the Graphics.PageUnit property. The Graphics.PageUnit is the unit of measure of the drawing surface-we can set this unit of measure to Inch, Pixel, Display (1/96 inch), Millimeter, etc.

One approach that makes it easy to coordinate the drawing of graphics and the drawing of text is to set the unit of measure of the drawing surface (the Graphics.PageUnit) and the unit of measure of the font (an argument to the constructor) to the same thing. Both settings are of type GraphicsUnit, so we can use the same GraphicsUnit enumeration to achieve this.

So, for example, suppose we are drawing a graphic such as picture of a ruler, which is marked with inch-marks along its edge. We can set the Graphics.PageUnit property to GraphicsUnit.Inch, and then express the coordinates of the drawing operations in terms of inches. We then can set the unit of measure of the font to GraphicsUnit.Inch, and express the size of the text in terms of inches.

Consider the following code:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.PageUnit = GraphicsUnit.Inch;

    Pen p = new Pen(Color.Black, 1/96f);
    Font f = new Font ('Times New Roman', 16);
    String s = 'Abc';
    SizeF sf = g.MeasureString(s, f);

    g.DrawRectangle(p, 1, 1, sf.Width, sf.Height);
    g.DrawString(s, f, Brushes. Black, 1, 1);

    f.Dispose();
    p.Dispose();
}

First, we set the unit of measure of the drawing surface to Inch. Then we express the pen width in terms of inches. Given that GDI+ defines the resolution of windows to be 96 DPI, it makes sense to create a pen with a width of 1/96 inch.

Note that this does not depend on the setup of the user's computer. GDI+ defines all screens to be 96 DPI, regardless of the actual resolution of the device itself.

Then, we create a font. After that, when we use the DrawString method to draw the string, we specify the drawing location in terms of inches too-marking the position (1, 1) (with the coordinates measured in inches).

But note that when we created our font, we expressed the size in terms of points, not inches. When we run this code, we will see the string drawn in 16 point Times New Roman, 1 inch from the top of the window, and 1 inch from the left of the window:

Click To expand

Clearly, the choice of unit of measure for the coordinate system for the drawing surface is independent of how we express things like the font size. But we can make them the same by setting the PageUnit property of the Graphics object, and then passing in the same GraphicsUnit as an argument to the Font constructor, like this:

Pen p = new Pen(Color.Black, 1/96f);
Font f = new Font("Times New Roman", 0.5f, GraphicsUnit.Inch);
String s = "Abc";
SizeF sf = g.MeasureString(s, f);

Now, the font size is expressed in terms of inches. The above example draws text that is 1/2 inch high one inch from the top of the window, and one inch from the left of the window:

Click To expand


Summary

We've covered a lot of ground in this chapter, establishing some important definitions and illustrating useful techniques. We began by introducing the Unicode character encoding system-a system that maps every typewritten character to a unique 16-byte code. Then we established the difference between five crucial concepts: character, glyph, font, font family, and typeface. A glyph is a graphic symbol that represents a character, a font is a set of glyphs, and a typeface is a set of fonts that are identical except in size. A font family is a collection of similar typefaces, which differ only in (bold or italic) style.

We took a look at the process of specifying font families and creating a font from a given font family. The Font class has 13 different constructors-over the course of the chapter we saw quite a few of them in use. They help us to achieve simple effects like bold, italic, underline, and strikeout, and a range of more complex effects too.

We saw an example that uses the system's 'default' font, and examined more closely how we use the DrawString method to draw text to our drawing surface. Typically, we need to specify things like the text string, a font, and a brush; we must also use the coordinate system of the drawing surface, of course, to specify the desired position of the text on the drawing surface.

We looked at the rectangle that bounds a text string. It's important to be able to control the dimensions of the rectangle in which a text string is to be placed. We often need to be able to dictate the maximum width that the text string occupies, or the maximum number of lines. We saw how we can use the Graphics.MeasureString method to get a Size object that tells us how a string is likely to be rendered, and to use the dimensions of the Size object actually to draw the text string.

We also saw how to force line-breaks using the \n string. We used the StringFormat class to create a whole range of different formatting effects: orienting the text vertically, drawing with and without line-wrap, and horizontal and vertical text alignment. We also used the StringFormat class, with the String.Format property, to apply a standard formatting to a number of text strings-our example used this technique to build a table effect.

Then we turned our attention to fonts. We saw that we can use the InstalledFontCollection class to establish which fonts are loaded on a system. Then we began a careful study of font metrics. We saw that a font size is determined by something called the em (thus, for a 12 point font the length of the em unit is 12 points). We looked at design units and resolution, and how the precise placement of text on a page (relative to other elements) can be achieved by a conversion between the font metrics of the font family (in design units) and the units used to draw the font instance.

We took a look at how anti-aliasing technology has reached new levels of sophistication in GDI+. Using ClearType anti-aliasing, we can achieve a very good quality of text using liquid crystal displays (LCDs). Finally, we took a look at the advantages of using the same unit of measure for the drawing surface and for the font.

We haven't covered every last detail of drawing text. GDI+ has capabilities for advanced text drawing that are mostly of interest to developers writing advanced typography applications, such as page layout programs, word processors, and the like. However, we have covered sufficient material that we should easily be able to satisfy the needs of most custom controls.


Chapter 5: Images

Highlights

Whether you are developing a rich-client application or a web application, the proper use of some well-designed images will considerably enhance the appeal and impact of your Windows application.

For example, if you're designing a rich-client application, you could use GDI+ to display an image in a button, or in a cell (within a spreadsheet), or as part of a node in a hierarchical tree control. If you're building an ASP.NET web application, you could customize a web page for a specific visitor by populating it with images that are generated dynamically on the web server using GDI+. It's easy to imagine how we'd employ this technique in a number of different types of application-for example, in an application for charting stock prices, or one that shows custom maps to the user, with the maps being generated dynamically according to the user's needs.

Thus, in this chapter we will focus on two GDI+ classes that allow us to handle images: the Image class and the Bitmap class. We'll start with a brief overview of image handling in GDI+ and the various types of bitmap files that we can use. We will then investigate the functionality that these classes provide, and how we can use it for the following purposes:

  • Displaying images. Some applications just need to be able to display images. For example, a real-estate application might display an image of the property for sale (and make the image available for printing). We'll investigate the GDI+ options for controlling the size, scale, resolution, and quality of our images.

  • Manipulating images. Some applications need to be able to manipulate images. We'll see how to crop, skew, rotate, and clone images, and how to create thumbnail images.

  • Loading and saving images. Some applications need to be able to store their drawing work to some kind of file or other permanent storage or to load files from disk for use at run time. We'll see how to perform both of these operations.

Moreover, some applications don't draw images direct to the screen. Instead, they draw the object ‘abstractly'-in memory-and draw the prepared image to the screen (or to disk) when it's complete. As described above, we may prepare the image dynamically on the web server, at run time, and then retrieve it to be displayed on a web page. Preparing images off-screen can give a considerable performance benefit-especially for complex, layered graphics. We'll see how GDI+ allows us to draw images ‘abstractly'-by using a Bitmap object as the drawing surface.

We'll then move on to take a brief look at the use of animated graphics. Certain types of animations, such as GIF animations, are stored in image files and we'll cover how to start and control a GIF animation in a rich-client application.

It's also useful to be able to load and save images in various formats, and know how to save an image to a different format from the one in which it was supplied. In particular, when building images to be included as part of a web page, we want to be very specific about exactly what type of image we build. Different image types have difference storage requirements, and therefore require more or less time to transmit to a browser. We'll finish off the chapter with a look at how we can use GDI+ to load images in one format, and save them in another format.


An Overview of Image Handling

We can break down the task of ‘image handling' into a number of fairly general but well-defined sub tasks-loading, displaying, manipulating, and saving. That is, when we perform a task on an image, it fits into one of the following task categories:

  • Loading the image from a file or other source, or creating a new image

  • Displaying or rendering the image to screen, or to some other drawing surface

  • Manipulating the image-making changes to the image in memory

  • Saving the image-in-memory to a file or other permanent storage

GDI+ provides a handsome functionality set that allows us to perform all of these kinds of tasks, for a large number of different types of image. In this section, we'll take a look at the GDI+ classes that provide this functionality.

We'll also look at the different types of bitmap that we can work with, and the different pixel formats that are supported by a bitmap in GDI+.

The Image, Bitmap, and Metafile Classes

GDI+ provides two classes that we can use to represent images-the Bitmap class and the Metafile class. Both of these classes inherit from the Image class, and therefore share a certain amount of functionality. But in many ways, they are also very different:

  • The Bitmap class provides functionality for handling bitmaps. A bitmap is a raster-based image-as you'll recall from So, the Bitmap class encapsulates the basic functionality that allows us to control the fundamental characteristics of raster-based bitmaps, such as size, resolution, color depth, whether the bitmap contains alpha information, and whether it uses a color map. We'll look at all this functionality in detail over the course of the next few pages.

  • The Metafile class provides functionality for handling vector images. This class acts more like a recorder. As we use a Metafile object to perform drawing operations, the object records these operations in a queue. When we draw the Metafile object to a drawing surface, it replays the drawing operations, applying them one at a time to the drawing surface. If there are any active transformations (such as rotations, scaling, or translation) applied to the drawing surface, then the transformations are applied separately to each individual drawing operation.

In the future, metafiles will be important in the implementation of scaleable icons for displays that have very high resolutions. For example, there are currently certain graphical items in Windows operating systems that are drawn in a hard-coded way with a fixed number of pixels-and yet are too small to be viewed comfortably on LCD screens with very high resolution. A solution to this problem would be to replace the existing raster-based icons with metafile icons-described using graphical operations expressed in vectors, and scalable up or down in size to any extent.

However, in this book, we are concerned chiefly with raster images, and so we will focus almost exclusively on the Image class and the Bitmap class.

The Image Class

The Image class is an abstract class, which means that we can't create Image objects directly-we can only create instances of the Bitmap and Metafile classes that inherit from it.

We can use an Image object (or, to be more accurate, a Bitmap or Metafile object) for two purposes: we can draw an image to a drawing surface, and we can use an image as a drawing surface. The abstract Image class encapsulates some basic functionality that is inherited by both the Bitmap and Metafile classes. This functionality includes expected properties such as the Height and Width of the image and the Palette, and a number of methods that either tell us about the image (for example, whether it contains alpha information, and whether the image uses a color map) or perform operations (for example, to Clone or Save the image).

The Bitmap Class

As we've already remarked, a Bitmap object is a representation of a raster image-that is, it represents a rectangular grid of pixels. (For a discussion of raster-based images and a comparison with vector-based images, see Chapter 2.) The image representation itself is in the form of an array of bits, which dictate the color of each pixel. Each pixel's color is represented by a fixed number of bits-as we'll see later on, in the Pixel Format section.

A Bitmap object is actually a representation of a bitmap image maintained in memory at run time. We can use the Bitmap object's functionality to load an image from disk or create a completely new image. Once we've done that, we can use the functionality to perform lots of other tasks on the image in memory. For example:

  • We can manipulate the image (for example, by drawing more elements to it, or by cropping it)

  • We can set or get individual bits of the image, or other properties of the image such as its size

  • We can draw the image to another drawing surface

  • We can save it, in a specified format (which may or may not require conversion), to a file or a stream

Let's take a look at a very simple example. We'll create a Bitmap object and use it to load a bitmap from a file into memory; then we'll use the Graphics class to display the image in memory onto our drawing surface:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");
    g.DrawImage(bmp, 0, 0);
}

The new Bitmap object we've created here is based on the specified JPEG file, rama.jpg. We could use another type of file, if the appropriate encoder/decoder is available on the system. We'// discuss encoders and decoders a few pages from here.

Before you can run this example, you'll need to put your image file into the directory where the application will run. If you're running the example in debug mode, then place rama.jpg into the /bin/debug subdirectory of your project directory. (Alternatively, you can provide the full path to the image file.) When run, this code will draw the image to your form, with the upper left corner of the image located at pixel (0, 0) in the client window, as specified in the DrawImage method call:

Click To expand

The image is of a very sweet, obedient dog, who was rescued from the gutter in India. His name is Rama.

This is about the simplest possible application that we can use to read and display an image. As you'd expect, we can control the display of the image much more closely than we have done in this example, and we'll begin to do that in the Displaying Images section.

Different Types of Bitmap

The following table describes the bitmap file formats supported by GDI+. In the last two columns, the table also lists the possible ‘bits-per-pixel' storage formats, and the type of compression supported by each. We'll discuss the meaning of these two columns in a moment:

Format | Description | Bits per Pixel | Compression
BMP | This is a standard format used by Windows. | 1, 4, 8, 15, 24, 32, 64 | None
EXIF | Exchangeable Image File is a variation of the JPEG file format used by many digital cameras. It contains additional information about the image (such as the date taken, shutter, etc.), and about the camera (such as the manufacturer and model). | 24 | Lossy
GIF | Graphics Interchange Format is a common format for web pages. It is appropriate for screenshots and line drawings. In addition, GIF can contain multiple images that can be played as an animation. | 1, 2, 4, 8 | Lossless
JPEG | Joint Photographic Experts Group is another common format for web pages. It is best for pictures. | 24 | Lossy
PNG | Portable Network Graphics is similar to GIF, but has more features. It supports greater color depth, and it enables progressive image display, which we might do when transmitting over a slow network connection. | 8, 24, 48 for color 1, 2, 4, 8 for grayscale | Lossless
TIFF | Tag Image File Format is an older format. TIFF files are often used in desktop publishing, faxing, 3D applications, and medical imaging. It supports a wide variety of color depths and compression. | Various | Lossless, Lossy

Color Depth

When we store a bitmap in a persistent storage medium such as a file on disk we often have the option to specify the color depth of the image-that is, the number of bits per pixel. As we mentioned in Chapter 2, if color is specified in RGB format, and we require 8 bits to specify the level of intensity of each of the red, green, and blue elements of the color, then we need 24 bits of disk space to store the color information for a single pixel. If the image contains Alpha information, then that's another 8 bits per pixel.

Sometimes, an image's color information is in the form of a color map (this is sometimes called a color table or a color palette). In this case, instead of storing the red, green, and blue values for each pixel, a bitmap instead stores an integer for each pixel-the integer is an index into the color map (and hence represents a color). If the total number of colors in the image is less than 256, then the color table needs at most 256 colors in it, and hence the index for each pixel can contained in a single byte (8 bits). Thus, we can get a significant reduction in file size compared to a bitmap that stores 24 or 32 bits per pixel.

If the bitmap is a monochrome bitmap, and uses only two colors, then we can use one bit per pixel for the color.

The decision as to whether to implement a color map for a bitmap (and hence restrict the number of colors and the size of the file) is influenced less these days by the amount of memory required to store the image, but more by the need to keep the images small enough that they can be transmitted efficiently over the Internet. JPEG files do not use color maps, but GIF files can. Color maps are best suited for simple images, such as graphs and other line drawings, that don't use a large number of colors.

Compression Formats

GDI+ supports many of the standard file formats for storing bitmaps. Many of these file formats will support one of the following types of compression:

  • Lossy compression-some information may be lost in the compression, but for certain types of image, this lost information is indiscernible. Photographs lend themselves to lossy compression. There are many levels of lossy compression-the greater the compression, the more information is lost, and the further the compressed image deviates from the original.

    If we use lossy compression on an image that has regular rectangular patterns, we will see an effect called pixilation around the lines. The lines will not look so neat, and we may see something similar to shadows of the lines, which is the pixilation effect.

  • Lossless compression-this takes advantage of the regularity of rectangular patterns within the image. Long horizontal or vertical runs of a single color can be represented in a parameterized manner, resulting in a significant reduction in file size. Traditionally, screenshots and computer-generated graphics are compressed effectively using lossless compression. However, with the advent of much fancier user interfaces (like those in Windows XP, with graduated title bars, and controls drawn with gradients), lossless compression will be less and less effective.

Encoders and Decoders

When we're using a Bitmap object to manipulate an image, we don't really care about the format that GDI+ uses internally-we just interact with the image through the Bitmap object, and allow GDI+ to worry about the storage details. However, when it comes to saving and loading images, we care very much about the format of the images. For example, if we're saving an image for consumption by another program, or with a certain storage requirement profile, then we need to take control of the way in which the image is saved.

In GDI+, we use encoders to save images to files, and we use decoders to load images from files. Sometimes, when we're talking about encoders and decoders, we use the term codecs-this is a generic term for both encoders and decoders.

The Encoder and Decoder classes enable us to extend GDI+ to support any image format. The .NET Framework provides us with a selection of encoders and decoders for different image formats-and if we need an encoder or decoder that is not supplied by the .NET Framework, we have the potential to be able to write our own.

So, how do we find out what codecs are available on our system? We can use the ImageCodecInfo class, as shown in the following code:

// get an array of available codecs.
ImageCodecInfo[]availableCodecs;
availableCodecs = ImageCodecInfo.GetImageEncoders();
int numCodecs = availableCodecs.Length;

for ( int i = 0; i < numCodecs; i++ )
{
    Console.WriteLine("Codec Name = " + availableCodecs[i].CodecName);
    Console.WriteLine("Class ID = " + availableCodecs[i].Clsid.ToString());
    Console.WriteLine('Filename Extension = "+
        availableCodecs[i].FilenameExtension);
    Console.WriteLine('Flags = " +
        availableCodecs[i].Flags.ToString());
    Console.WriteLine("Format Description = " +
        availableCodecs[i].FormatDescription);
    Console.WriteLine('Format ID = " +
        availableCodecs[i].FormatIDD.ToString());
    Console.WriteLine("MimeType = " + availableCodecs[i].MimeType);
    Console.WriteLine("Version = " +
        availableCodecs[i].Version.ToString());
    Console.WriteLine();
}

The ImageCodecInfo is in the System.Drawing.Imaging namespace, so to run this example, you need to add a using statement at the top of the file (if it's a console application, you'll first need to add references to the System.Drawing.dll and System.Drawing.Design.dll).

This is the output that I get when I run this code on my machine. When you run the code, you should get something similar:

Codec Name = Built-in BMP Codec
Class ID = 557cf400−1a04−11d3−9a73−0000f81ef32e
Filename Extension = *.BMP;*.DIB;*.RLE
Flags = Encoder, Decoder, SupportBitmap, Builtin
Format Description = BMP
Format ID = b96b3cab−0728-11d3-9d7b-0000f81ef32e
MimeType = image/bmp
Version = 1

Codec Name = Built-in JPEG Codec
Class ID = 557cf401−1a04−11d3−9a73−0000f81ef32e
Filename Extension = *.JPG;*.JPEG;*.JPE;*.JFIF
Flags = Encoder, Decoder, SupportBitmap, Builtin
Format Description = JPEG
Format ID =
MimeType = image/jpeg
Version = 1

Codec Name = Built-in GIF Codec
Class ID =
Filename Extension = *.GIF
Flags = Encoder, Decoder, SupportBitmap, Bultin
Format Description = GIF
Format ID =
MimeType = image/gif
Version = 1

Codec Name = Built-in TIFF Codec
Class ID =
Filename Extension = *.TIF;*.TIFF
Flags = Encoder, Decoder, SupportBitmap, Builtin
Format Description = TIFF
Format ID =
MimeType = image/tiff
Version = 1

Codec Name = Built-in PNG Codec
Class ID =
Filename Extension = *.PNG
Flags = Encoder, Decoder, SupportBitmap, Builtin
Format Description = PNG
Format ID =
MimeType = image/png
Version = 1

From this output, we can see that on my machine I have codecs for BMP, JPEG, GIF, TIFF, and PNG file types.

Pixel Format

Having discussed the subject of image file formats, it's worth also taking a look at the subject of pixel formats. Every image has an associated pixel format. The pixel format of the image specifies the color depth of the image, whether the colors are indexed with a color map, and whether the image contains alpha information.

When we read an image from a file, the file contains the pixel format of the image. When the image is held in memory, the pixel format is also held in memory-it's an integral part of the makeup of the image.

To find out about the pixel format of an image, we can use the PixelFormat property (this is a property of the Image class, and therefore inherited into the Bitmap and Metafile classes). When we create an image, we can specify our desired pixel format, or we can use a constructor that gives us an image that matches the pixel format of a specified drawing surface. When we clone an image (that is, when we use an existing image to create a new one), we can specify the pixel format for the new cloned image explicitly-which means that we can change it if we want.

The PixelFormat property contains a value that belongs to the PixelFormat enumeration (which is part of the System.Drawing.Imaging namespace). The PixelFormat enumeration contains three different types of member-we'll look at all three here.

Available Pixel Formats

First, we have the following set of members, which define the supported values for pixel format. The member names have been designed to summarize the pixel format they represent, and the naming convention works like this:

  • First, all of these members have names that are prefixed with the word Format.

  • The next part of the name is something like 8bpp or 32bpp-this portion of the name indicates the number of bits per pixel (that is, the amount memory that the format uses to store the color information for a single pixel).

  • The next part describes the type of information that the bits represent:

    • Rgb means that the bits-per-pixel storage is broken down into red, green, and blue components

    • Argb means that each pixel also has an alpha component

    • PArgb means that the red, green, and blue components are pre-multiplied according to the alpha component

    • Indexed means a color table is used

    • Grayscale means there is no color, but the specified number of shades of gray

  • Finally, if there is a sequence of integers at the end (555, 565, 1555), then it represents the breakdown of red-green-blue (or alpha-red-green-blue) components in a situation where they're not split equally across the available bits-per-pixel.

The full list of 14 pixel-format members is listed here:

PixelFormat Member | Color Depth Description
Format24bppRgb | 8 bits of red, 8 bits of green, 8 bits of blue
Format32bppRgb | 8 bits of red, 8 bits of green, 8 bits of blue, 8 bits not used
Format48bppRgb | 16 bits of red, 16 bits of green, 16 bits of blue
Format32bppArgb | 8 bits of red, 8 bits of green, 8 bits of blue, 8 bits of alpha
Format64bppArgb | 16 bits of red, 16 bits of green, 16 bits of blue, 16 bits of alpha
Format32bppPArgb | 8 bits of red, 8 bits of green, 8 bits of blue, 8 bits of alpha. The red, green, and blue components are pre-multiplied according to the alpha component.*
Format64bppPArgb | 16 bits of red, 16 bits of green, 16 bits of blue, 16 bits of alpha. The red, green, and blue components are pre-multiplied according to the alpha component.*
Format16bppRgb555 | 32,768 shades of color (5 bits of red, 5 bits of green, 5 bits of blue, and 1 bit not used).
Format16bppRgb565 | 65,536 shades of color (5 bits of red, 6 bits of green, 5 bits of blue).
Format16bppArgb1555 | 32,768 shades of color (5 bits of red, 5 bits of green, 5 bits of blue, and 1 bit of alpha).
Format1bppIndexed | Two shades of color using a color table.
Format4bppIndexed | 16 colors using a color table.
Format8bppIndexed | 256 colors using a color table.
Format16bppGrayScale | 65,536 shades of gray.

* Pre-multiplication according to the alpha component is a technique that involves scaling the three color components by the alpha component before storing their values. If we intend to use alpha-blending (that is, to display two overlapping images, using the alpha components of the two images to create an effect in which they're both semi-transparent and hence 'blended' where they overlap) then pre-multiplication saves a significant amount of processing time. Otherwise, pre-multiplication is not required.

Note that if an image is stored with pre-multiplication and a low alpha value, colors may not show true if the image is subsequently shown at full opacity.

Capabilities of an Image

If we want to test an image to determine whether it has particular capabilities, we can test for the following PixelFormat enumeration members in the PixelFormat property:

PixelFormat Member | Description
Alpha | Used to determine whether an image contains alpha information.
PAlpha | Used to determine whether an image contains alpha information, and if so, whether the color components are pre-multiplied according to the alpha component.
Indexed | Used to determine whether an image has a color table. Some operations, such as drawing with gradient brushes, are not allowed on images that have a color table.

To implement these tests, we 'bitwise AND' the pixel format from the image with one of the above members-if the result is non-zero, then we interpret that as an affirmative.

Here's an example. In the following code, we create two new images-one with an alpha component, and the other without. We then test the two images following the above procedure.

// Create two new bitmap images
Bitmap bmp1 = new Bitmap(100, 100, PixelFormat.Format32bppArgb);
Bitmap bmp2 = new Bitmap(100, 100, PixelFormat.Format24bppRgb);

// Test for Alpha
bool b1 = ((bmp1.PixelFormat & PixelFormat.Alpha) != 0);
bool b2 = ((bmp2.PixelFormat & PixelFormat.Alpha) != 0);

// Output results to console window
Console.WriteLine("bmp1 has alpha?: " + b1);
Console.WriteLine("bmp2 has alpha?: " + b2);

// Clean up
bmp1.Dispose();
bmp2.Dispose();

The output of this code is as follows:

bmp1 has alpha?: True
bmp2 has alpha?: False

Taking the Easy Option

Finally, there is one member of the PixelFormat enumeration that allows us to specify that we don't care what the pixel format is when we create an image:

PixelFormat Member | Description
DontCare | Indicates that we don't care what pixel format the image has.

Later on, we will use this member when we clone an image. Effectively, it means that we allow GDI+ to choose the pixel format of the newly cloned image.


Displaying Images

In this chapter, we've already seen one simple example that loaded an image from disk into memory (using a Bitmap object), and then drew that image onto the drawing surface. The entire example was only a few lines long, because we just wanted to show this simple concept in action.

In fact, we could have taken greater control of how the image was displayed, by adding extra code to affect the size, scale, resolution, and interpolation of the image that appears on the drawing surface. In this section, we'll take a look at these issues and how they affect the image display.

Checking the Size and Resolution

In that first example of this chapter, we just loaded the image and displayed it, and left all the complicated stuff to GDI+. Here's the code we used there:

Bitmap bmp = new Bitmap("rama.jpg");
g.DrawImage(bmp, 0, 0);

In order to process the image in memory and draw it to the drawing surface, GDI+ will have taken into account a number of factors: the image's (horizontal and vertical) size, the image's resolution, and the resolution of the drawing surface. All that took place behind the scenes.

Let's take a more active interest in what GDI+ is doing here. The bitmap image's size (its width and height) are accessible through the Width and Height properties of the Bitmap object-the image is raster-based and these values are given in pixels.

The image is also stored at a certain resolution-that is, it has an associated dots-per-inch (DPI) value. In fact, the image must have a certain number of dots in the horizontal direction, and a certain number of dots in the vertical direction-hence, the Bitmap class makes these values available to us through the HorizontalResolution and VerticalResolution properties. If the pixels are square (which is usually the case), then these two values are the same, and we just talk about the image's resolution.

Furthermore, the drawing surface (in our case, the screen) also has a resolution. As we've discussed elsewhere, the image resolution of a screen is assumed to be 96 DPI (see the discussion on screen resolution in Chapter 2). Whatever the drawing surface, we can obtain the horizontal and vertical resolution of the drawing surface through the DpiX and DpiY properties of the Graphics class.

Now, as we said, GDI+ automatically calculates the display size of the image on our screen based on the size of the image, the resolution of the image, and the resolution of the screen. The default behavior is to use the following formulae:

Click To expand

These equations are derived from the fact that by default, the ratio between the display image size and the actual image size is equal to the ratio between the display resolution and the image resolution. In other words:

Click To expand

To see this working, let's add some code to our example. The following shaded lines tell us the screen resolution and image resolution, and the width and height of the image contained in memory in the Bitmap object. Then we use a Size object to calculate the display width and height of the image on the screen:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap('rama. jpg');
    g.DrawImage(bmp, 0, 0);

    Console.WriteLine("Screen resolution: " + g.DpiX + "DPI");
    Console.WriteLine("Image resolution: " +
                            bmp.HorizontalResolution + "DPI");
    Console.WriteLine("Image Width: "  + bmp.Width);
    Console.WriteLine("Image Height: ' + bmp.Height);

    SizeF s = new SizeF(bmp.Width*(g.DpiX/bmp.HorizontalResolution),
                        bmp.Height*(g.DpiY/bmp.VerticalResolution));
    Console.WriteLine('Display size of image: " + s);
}

When we run this code, it will display the image and also write the output to the console window:

Screen resolution: 96DPI
Image resolution: 300DPI
Image Width: 1000
Image Height: 1100
Display size of image: {Width=320, Height=352}

Of course, the exact output will depend on the size and resolution of the image you're using. This output tells us that the image of Rama the dog is 300DPI and 1000×1100 pixels. The main point of this exercise is to show that by default, GDI+ uses the ratio between the display resolution and the screen resolution to work out exactly how to display the image on the screen.

Controlling the Scaling of an Image

Unplanned automatic scaling, in the manner described above, incurs a performance penalty so we may want to avoid it. One alternative is to create a resized copy of the bitmap ahead of time-either in memory or on the disk-so that we can then draw it without scaling. This technique doesn't alter the final appearance of the bitmap; it simply improves the performance profile.

We can control the scaling explicitly when we display an image. The DrawImage method has 16 overloads, and some of these overloaded methods allow us to specify both a source rectangle and a destination rectangle. The source rectangle refers to the image, and allows us to specify a subset of the source image to draw. If we specify a rectangle that is the same size as the image itself, then DrawImage will draw the entire image.

The destination rectangle refers to the drawing surface. If we specify that the destination rectangle should be the exactly same size (in pixels) as the source rectangle, then DrawImage will draw the image without any resizing at all. If we specify a destination rectangle that is smaller or larger than the image size, then DrawImage will perform the appropriate scaling (to make the source image fit the destination rectangle), before it draws the image. If we omit to specify the destination rectangle, GDI+ will compare the resolution of the image and the resolution of the drawing surface, and decide whether or not to scale the image for us.

Displaying an Image without Scaling

In the following example, GDI+ draws an image, pixel for pixel, to the form. We create a rectangle whose dimensions are the same as the dimensions of the image, and then use that rectangle as both source and destination rectangle. Because the dimensions of the destination rectangle are the same as those of the image itself, this method disregards the resolution of the image and the window, and draws the image verbatim:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");

    Rectangle r = new Rectangle(0, 0, bmp.Width, bmp.Height);
    g.DrawImage(bmp, r, r, GraphicsUnit.Pixel);
}
Scaling the Image to Fit the ClientRectangle

Another common operation is to draw an image so that it takes up the client area of a form or a custom control. We can use another overload of the DrawImage method to do this:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");

    Rectangle r = new Rectangle(0, 0, bmp.Width, bmp.Height);
    g.DrawImage(bmp, this.ClientRectangle);
}

Now, when we resize the window, we see our image resized to fill the client area of the window:

Click To expand

No animals were harmed during the production of this book…

Changing the Resolution of a Bitmap

We can change the resolution of a bitmap by calling the SetResolution method of the Bitmap object. When we change the resolution of the bitmap, it affects how GDI+ resizes the bitmap when it draws it. To demonstrate this, we can modify the Paint event from the last example, as follows:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");

    g.FillRectangle(Brushes.White, this.ClientRectangle);
    bmp.SetResolution(600f, 600f);
    g.DrawImage(bmp, 0, 0);
    bmp.SetResolution(1200f, 1200f);
    g.DrawImage(bmp, 180, 0);
}

When this code is run, we can see the following results:

Click To expand

The resolution of the original image of Rama the dog (rama.jpg) is 300 DPI, and the image is 1000×1100 pixels. Hence the width and height of the image in memory are:

Width = 1000/300 = 3.333 inches

Height = 1100/300 = 3.667 inches

We've obtained the image on the left by changing the resolution of the image in memory to 600 DPI, and displaying the result on the drawing surface. The change of resolution has the effect of changing the ‘physical dimensions' of the bitmap in memory to:

Width = 1000/600 = 1.667 inches

Height = 1100/600 = 1.833 inches

The image on the right is obtained in the same way-this time we changed the resolution of the image in memory to 1200 DPI, which has the effect of changing the ‘physical dimensions' of the bitmap in memory to:

Width = 1000/1200 = 0.833 inches

Height = 1100/1200 = 0.916 inches

The point here is that GDI+ pays attention to the resolution of the bitmap, and if we don't do any explicit scaling, GDI+ will use the resolution of the bitmap to scale the image.

Image Resizing and Interpolation

As we've seen above, sometimes we scale an image explicitly, and sometimes GDI+ implicitly scales the image as a consequence of the drawing operations we've requested. Either way, whenever an image is resized, GDI+ uses one of several algorithms to do the resizing. When GDI+ needs to resize an image to produce a larger image (with a greater number of pixels), it uses an image manipulation algorithm to determine the color of the new pixels. This process is called interpolation.

Interpolation is the process of using the known values of two points to estimate or calculate the values at other points that lie between. A demographer who knows the population of a town in the year 1960 and the year 1965, he can use those values to reasonably estimate what the town's population was in, say 1962. When we increase the resolution of an image, some of the pixel points in the new image correspond to pixel points in the old image, and so we can consider that we know the colors of those pixel points. But the new image, being bigger, has new pixel points whose color we don't know. So, GDI+ uses the color values of nearby pixels, and applies similar interpolation techniques to determine the colors of those new pixels.

Using Interpolation Mode to Control Image Quality

When resizing an image-and particularly when enlarging one-there is always a tradeoff between processing time and image quality. An algorithm that accurately interpolates the values of new pixels will create a higher-quality image, but will take longer. We might not need to worry at all about using CPU time when using Windows Forms and resizing a few images. However, when using GDI+ to generate images dynamically on a busy web server, we need to consider the tradeoff between processing time and image quality.

For this reason, GDI+ allows us to control the image resizing quality programmatically, through the value we assign to the Graphics.InterpolateionMode property. This property takes a value from the InterpolationMode enumeration (which belongs to the System.Drawing.Drawing2D namespace). There are five values in this enumeration, which denote a specific level of quality, and in ascending order of quality, they are:

NearestNeighbor
Bilinear (the default)
HighQualityBilinear
Bicubic
HighQualityBicubic

In addition, there are three more values in the InterpolationMode enumeration, which we can use here. They are:

Default
High
Low

To see the Graphics.InterpolationMode property in action, let's modify our previous example a little further:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    int width = bmp.Width;
    int height = bmp.Height;

    // resize the image using the lowest quality interpolation mode
    g.InterpolationMode = InterpolationMode.NearestNeighbor;
    g.DrawImage(
        bmp,
        new Rectangle(10, 10, 120, 120),    // source rectangle
        new Rectangle(0, 0, width, height), // destination rectangle
        GraphicsUnit.Pixel);

    // resize the image using the highest quality interpolation mode
    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
    g.DrawImage(
        bmp,
        new Rectangle(130, 10, 120, 120),   // source rectangle
        new Rectangle(0, 0, width, height), // destination rectangle
        GraphicsUnit.Pixel);
}

When you run the example, you should see something like the following screenshot. The difference in quality may not be too clear in this screenshot, but you should be able to see on your screen that the image on the right is of far higher quality.

Click To expand


Manipulating Images

When we use GDI+'s Bitmap object to represent a bitmap image stored in memory, there are plenty of operations that we can perform to manipulate that bitmap. For example, we can crop it, skew it, reflect it, and clone it-we can even retrieve thumbnail information from it. We'll see examples of all these in this section.

Cropping an Image

Cropping an image is a common tool in image manipulation-quite simply, it involves selecting a rectangular portion of the image, and trimming (and throwing away) the parts of the image that lie outside that rectangle. The size of the resulting, cropped image is, naturally, smaller than the original- it's the same size as the rectangle we used to define the crop.

The following code draws a cropped image:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Rectangle sr = new Rectangle(80, 60, 400, 400);
    Rectangle dr = new Rectangle(0, 0, 200, 200);
    g.DrawImage(bmp, dr, sr, GraphicsUnit.Pixel);
}

The source rectangle, sr, is a rectangle whose top-left corner is at location (80, 60) and whose dimensions are 400×400 pixels. This rectangle defines the portion of the original image that we want to keep after we've cropped it-as you can see, it contains Rama's right ear. The destination rectangle, dr, describes the location within the drawing surface where we want to draw the result, and the size we want it to be. The result looks like this:

Click To expand

Skewing, Reflecting, and Rotating an Image

One of the overloaded DrawImage methods allows us to pass an array of three points as an argument. These three points perform the following function when the image is drawn:

  • The first point specifies the destination of the upper-left point of the original bitmap

  • The second point specifies the destination of the upper-right point of the original

  • The third point specifies the destination of the lower-left point of the original

We can scale or skew the image, or reflect it either horizontally or vertically, or rotate it, simply by choosing the appropriate values for these three destination points. In fact, if we wish, we can achieve any combination of these manipulations on an image, simultaneously, within just a single operation based on the values of our three points. (For the purposes of simplicity in this section, we'll deal with them one at a time.)

Skewing an Image

First, let's try skewing our image of Rama the dog. Here's some code that will do the job:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Point[] destinationPoints = {
        new Point(0, 0),    // destination for upper-left point of original
        new Point(100, 0),  // destination for upper-right point of original
        new Point(50, 100)};// destination for lower-left point of original
    g.DrawImage(bmp, destinationPoints);
}

When run, it looks like this. As you can see, we've set the destination points of three of the corners of the image in such a way that GDI+ must skew the image into a parallelogram shape.

Click To expand

Reflecting an Image

Now, let's modify the array of points in order to create the effect of a reflection in a horizontal mirror. To do this, we tell GDI+ that the destination of the top-left corner is below the destination of the bottom-left corner on the drawing surface:

Point[] destinationPoints = {
    new Point(0, 100),  // destination for upper-left point of original
    new Point(100, 100),// destination for upper-right point of original
    new Point(0, 0)};   // destination for lower-left point of original
g.DrawImage(bmp, destinationPoints);

The result is that we see Rama's reflection in a puddle.

Click To expand

Rotating an Image

If we want to rotate the image 90 degrees to the right, we would set up the array as follows:

Point[] destinationPoints = {
    new Point(100, 0),  // destination for upper-left point of original
    new Point(100, 100),// destination for upper-right point of original
    new Point(0, 0)};   // destination for lower-left point of original
g.DrawImage(bmp, destinationPoints);

This simply places the upper-left corner of the source in the upper right-corner of the destination; and the lower-left corner of the source in the upper-left corner of the destination, and so on.

Click To expand

The result is similar to the effect we get when we use the Bitmap.RotateFlip method. However, there is an important difference to note. Here, were using the DrawImage method to manipulate the image as we're drawing it to the drawing surface-so it doesn't affect the image stored in memory. By contrast, the Bitmap.RotateFlip method is a manipulation of the image stored in memory itself.

Cloning an Image

The abstract Image class provides a Clone method, which allows us to make a copy of an existing Image, Metafile, or Bitmap object. In fact, the Clone method is overloaded, so there are a number of ways we can use it. We will not cover them all here-they're all listed in the GDI+ documentation at http://msdn.microsoft.com/library.

However, we will examine one example here. The Clone method has an overload that allows us to specify a source rectangle with which we can specify the portion of the source image-in this case, the Clone method produces a clone of the specified portion.

The following code demonstrates how to clone a portion of an existing Bitmap object, to create a new Bitmap object:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap('rama.jpg');
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Rectangle r = new Rectangle(120, 120, 400, 400);
    Bitmap bmp2 = bmp.Clone(r, System.Drawing.Imaging.PixelFormat.DontCare);
    g.DrawImage(bmp, new Rectangle(0, 0, 200, 200));
    g.DrawImage(bmp2, new Rectangle(210, 0, 200, 200));
    bmp2.Dispose();
}
Important

When we specify the source rectangle, we need to make sure that the rectangle is within the bounds of the source image; if not, we will get a System.OutOfMemoryException exception.

When we run this, we see the two images. The image on the left is a display of the original; the one on the right is a display of the clone:

Click To expand

Getting a Thumbnail of an Image

Some image formats have the capability to contain more than just the image itself. In particular, the image source could contain information about multiple images, and in particular, a thumbnail image. The Image class provides us with a method called GetThumbnailImage, which allows us to get a thumbnail of the image. If the image source contains thumbnail data, then this method gets it. If not, then the method scales the image to create a thumbnail image.

Before we demonstrate this method, we need to make a brief point about the arguments that the method expects. Here is the signature of the GetThumbnailImage method:

public Image GetThumbnailImage(
   int thumbWidth,
   int thumbHeight,
   Image.GetThumbnailImageAbort callback,
   IntPtr callbackData
}:

The last two arguments of this method (callback, and callbackData) are not used in GDI+ version 1.0. Even though they are not used, they're not optional-so we still need to declare a delegate for the callback method that does nothing other than return false, and pass the delegate to this method.

Therefore, we first declare our ThumbnailCallback method, as follows:

public bool ThumbnailCallback()
{
    return false;
}

Now, we're in a position to write a brief example within our Paint event handler:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    Image.GetThumbnailImageAbort thumbnailCallback =
                   new Image.GetThumbnailImageAbort(ThumbnailCallback);
    Image tn = bmp.GetThumbnailImage(
                   40, 40, thumbnailCallback, IntPtr.Zero);
    g.DrawImage(tn, 0, 0, tn.Width, tn.Height);
    tn.Dispose();
}

In this code, we declare the delegate as follows:

Image.GetThumbnailImageAbort thumbnailCallback =
               new Image.GetThumbnailImageAbort(ThumbnailCallback);

Then, we use this delegate when we call the GetThumbnailImage method. After getting the thumbnail image, we draw it.


Creating and Drawing into an Image

Of course, we won't always want to work with images loaded from disk. Sometimes we'll want to draw our own images-and in that case, we'd need to create a brand new, empty image object and then perform a number of operations to build up the elements of our new image in memory. We may even want to combine images to create different and exciting effects.

So, in this section, we'll see how GDI+ allows us to do all these things.

Creating a New Bitmap

When we want to create a new bitmap object into which to draw, we can use one of the Bitmap class constructors. There are a variety of options available to us. There are three constructors that are of interest to us here, and the following three lines show how we would use each of them:

Bitmap bmp = new Bitmap(width, height);
Bitmap bmp = new Bitmap(width, height, pixelFormat);
Bitmap bmp = new Bitmap(width, height, g);

In all of these constructors, width and height must be integers-they indicate the width and height of the new image in pixels. If we use the first constructor, we get a new Bitmap object that represents a bitmap in memory with a PixelFormat of Formats32bppARGB.

If we use the second constructor, we can specify our own choice of PixelFormat-here, pixelFormat is a value that is taken from the PixelFormat enumeration, which we discussed earlier in this chapter.

If we use the third constructor, we get a new Bitmap object representing a bitmap in memory whose resolution and color depth are the same as those of the specified drawing surface (here, g is the Graphics object which represents the drawing surface).

Drawing our First Image

Once we've created the Bitmap object, we can get an instance of the Graphics class that encapsulates a drawing surface for the bitmap. Using this Graphics object, we can draw into the bitmap. Then, during the Paint event, we can use the Graphics object that represents the screen drawing surface to draw the image to the screen.

To make this clear, we can put it all into the Paint method handler like this:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Bitmap bmp = new Bitmap(100, 100);
    Graphics gImage = Graphics.FromImage(bmp);
    gImage.FillRectangle(Brushes.Red, 0, 0, bmp.Width, bmp.Height);
    gImage.DrawRectangle(Pens.Black, 10, 10, bmp. Width-20, bmp. Height-20);

    Graphics gScreen = e.Graphics;
    gScreen.FillRectangle(Brushes.White, this.ClientRectangle);
    gScreen.DrawImage(bmp, new Rectangle(10, 10, bmp.Width, bmp.Height));
}

Note in particular that we use two instances of the Graphics class here. The first, gImage, represents the drawing surface of the image. The second, gScreen, is more familiar to us-it represents the drawing surface of the form, which is the final destination of our image. Here's the result:

Click To expand
A Note on Scope

In the example above, we've put all the code here in the Paint event handler for simplicity. In a more realistic application, it often makes more sense to give the Bitmap object greater scope. We might create the Bitmap object at the Form-level, like this:

public class Form1 : System.Windows.Forms.Form
{
    private Bitmap bmp;
    ...
}

Then, if we wanted to create and draw the new image at the time we load the control, we'd use the Load event handler to do it, like this:

private void Form1_Load(object sender, System.EventArgs e)
{
    bmp = new Bitmap(100, 100);
    Graphics g = Graphics.FromImage(bmp);
    g.FillRectangle(Brushes.Red, 0, 0, bmp. Width, bmp. Height);
    g.DrawRectangle(Pens.Black, 10, 10, bmp.Width-20, bmp.Height-20);
}

Finally, we'd paint the resulting bitmap to the drawing surface, through the Paint event handler:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    g.DrawImage(bmp, new Rectangle(10, 10, bmp.Width, bmp.Height));
}
Advantages of Drawing to a Bitmap Object

There are sometimes a number of benefits that we gain if we choose to create and draw into a bitmap and then draw the bitmap to the form (instead of drawing directly to the form):

  • Performance. We may wish to draw into an image ahead of time, and then draw the image in the Paint event when we need it. This means we have to draw the graphics one time only, instead of drawing them each time that we receive a Paint event. If the drawing is time-intensive, then this gives us a definite performance improvement.

  • Dynamic image generation. Sometimes we want to include a dynamically generated graphic in a web page. To do this, we draw into an image on the web server, and then include a reference to the dynamically generated image in our web page.

  • Preparing complex 3D graphics. If our graphic has multiple layers, where we first need to draw the background, then mid-ground, then foreground, drawing into an image off-screen allows us to prepare the image completely in advance of the time we render to the screen. This prevents the unsightly flashing effect that comes from painting one layer over another on the screen.

    Moreover, some image processes such as blurring and sharpening can only be achieved by manipulating the image directly before it's rendered. By drawing the image in advance, we make these facilities available to ourselves.

Double-buffering

If we need to create and present complex, layered graphics, then we surely must be concerned about the performance of our code. To create such graphics in a more efficient manner, we can use a technique called double-buffering.

Double-buffering is not really a very abstract process. In short, it is the process of drawing to an image, and then drawing the completed image to a form. Effectively, we use the image drawing surface as an off-screen buffer, which allows us to prepare the image completely before we paint it to the screen.

Double-buffering is particularly useful when we're drawing graphics that involve layering of many different elements, one on top of another, at the same location in the drawing surface, to build up a three dimensional effect. The problem is that creating such effects within a single image is a process that tends to take up a significant amount of processing time.

For example, consider a situation where we need to draw a complex map with many elements-rural and urban backgrounds, highways, rivers, symbols, road names, and area names. We want to avoid the job of drawing each of these elements individually onto a single image. There are two reasons for this: first, there's a great deal of extra effort involved in getting the relative levels of each element exactly right. Second, it's lots of work for the processor. On a slower processor, we could see these graphical operations being performed individually, one after another. This may cause a flickering effect because the user sees the graphic for a very small amount of time after the background is drawn, but before the foreground is drawn.

Instead, we can use double-buffering to build up the image in layers. We'd draw the background first; then geographical regions in a different color on top of the background; then, we might draw rivers, cities, highways, railroad tracks, and so on, on top of the geographical regions. Finally, we might add text labels to many of these points. Then, when everything is in place, we paint the whole lot to the screen in one go.

Double-buffering allows us to prepare our graphic ahead of time, and then draw it to the screen in a single operation. This will create a much smoother appearance to the user, and create a more efficient process.

In today's world of fast processors and 3D graphics accelerators, it is a little difficult to come up with an artificial example that demonstrates the benefits of double-buffering. After all, if CPUs and graphics accelerators are capable of running Quake III, we are going to need to throw quite a lot of graphics operations at them before they will even flinch! For this reason, I've elected not to include any further examples here. In fact, the example in the previous section demonstrates the principle of double-buffering: we prepared the red rectangle off screen in a Bitmap object, and we only displayed it to the screen after we'd completely finished it.

Using the SetStyle Method

Windows Forms and GDI+ support double-buffering seamlessly. We can set three flags, and then GDI+ will take care of all details of the double-buffering for us. To enable double-buffering, we need to use the SetStyle method of the Form class. Placed in either in the constructor for the form, or in the Load event for the form, the following code enables double-buffering:

SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);

The definitions of the members of the ControlStyles enumeration are:

ControlStyles Enumeration Member | Description
DoubleBuffer | Indicates that all drawing operations are to be performed in a buffer (an image), and that after drawing operations are complete, the result is drawn to the screen.
AllPaintingInWmPaint | Indicates that the background of the window should not be drawn in a separate operation. We should set this style only if we set UserPaint to true.
UserPaint | Indicates that the control (rather than the WindowsForms framework) should paint itself.

We set AllPaintinginWmPaint and UserPaint to true when our control draws its entire surface; the framework doesn't draw any part of the control, including the background of the control.

This is what happens when we set these styles. When GDI+ raises the Paint event, it refrains from setting the drawing surface to be the window: instead, it creates an image in memory, and a Graphics object that allows us to use that image as a drawing surface. Then it passes that Graphics object in as part of the argument to the Paint event.

This all happens behind the scenes. Our drawing code doesn't need to know that it is drawing to an image rather than directly to the screen. After the Paint event returns, the framework then draws the prepared image to the form or window.

Working with Alpha in Images

As we first mentioned in Chapter 2, we can use the alpha to control the level of transparency of a color. We saw how we can create a custom Color object which is composed of alpha, red, green, and blue components (and hence has a color and a level of transparency), and then assign the Color to a brush or pen and use it to draw lines, arcs, rectangles, text, and anything else to the drawing surface with a semi-transparent effect.

Whether the drawing surface is a form or an image in memory, these facilities work in much the same way. We draw elements to the drawing surface, and as a result, the individual pixels of the drawing surface are assigned a color and a level of transparency. In addition, we can use alpha in images to enable us to implement further special effects. For example, we can cause an entire image to be semi-transparent; and when we draw new elements into an existing image, we can draw semi-transparently-so that our drawing operations are blended with the existing contents of the image.

When we draw with a semi-transparent color (that is, a color that contains alpha information) into an image that already contains alpha information, then we need to think about how GDI+ calculates the color and alpha of the individual pixels in the resulting image. For each affected pixel, GDI+ needs to combine the existing color and alpha with the color and alpha of our drawing color, to produce a resultant color and alpha that has the effect we desire.

GDI+ will calculate the new colors of the affected pixels in one of two ways:

  • The drawing color and alpha may be blended with the existing color and alpha, to create a result that is dependent on the colors and alphas of both

  • Alternatively, the drawing color and alpha may simply overwrite the existing color and alpha in the pixel

We can control whether a drawing operation blends with the image, or overwrites its alpha information onto the image, through the CompositingMode property of the Graphics class. This property has two possible values, which belong to the CompositingMode enumeration (part of the System.Drawing.Drawing2D namespace). The values are:

CompositingMode Enumeration Member | Description
SourceCopy | The drawing color and alpha overwrite the existing bits in the image.
SourceOver | The drawing operation is blended with the existing pixels of the image. The blend is determined by the alpha component of the color being drawn.

We can build a small example to demonstrate this graphically. First, let's draw some vertical bars in a window:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics gForm = e.Graphics;
    gForm.FillRectangle(Brushes.White, this.ClientRectangle);
    for (int i = 1; i <= 7; ++i)
    {
        Rectangle r = new Rectangle(i*40-15, 0, 15,
                                    this.ClientRectangle.Height);
        gForm.FillRectangle(Brushes.Orange, r);
    }
}

When we run the example without drawing any images with alpha in them, it appears as follows:

Click To expand

Now, we'll create a bitmap in memory, and draw it over the vertical bars. To do that, we'll modify the Paint event as follows:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics gForm = e.Graphics;
    gForm.FillRectangle(Brushes.White, this.ClientRectangle);
    for (int i = 1; i <= 7; ++i)
    {
        Rectangle r = new Rectangle(i*40−15, 0, 15,
                                    this.ClientRectangle.Height);
        gForm.FillRectangle(Brushes.Orange, r);
    }

    // create a Bitmap image in memory and set its CompositingMode
    Bitmap bmp = new Bitmap(260, 260,
        System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    Graphics gBmp = Graphics.FromImage(bmp);
    gBmp.CompositingMode =
        System.Drawing.Drawing2D.CompositingMode.SourceCopy;

    // create a red color with an alpha component
    // then draw a red circle to the bitmap in memory
    Color red = Color.FromArgb(0×60, 0×ff, 0, 0);
    Brush redBrush = new SolidBrush(red);
    gBmp.FillEllipse(redBrush, 70, 70, 160, 160);

    // create a green color with an alpha component
    // then draw a green rectangle to the bitmap in memory
    Color green = Color.FromArgb(0×40, 0, 0×ff, 0);
    Brush greenBrush = new SolidBrush(green);
    gBmp.FillRectangle(greenBrush, 10, 10, 140, 140);

    // now draw the bitmap on our window
    gForm.DrawImage(bmp, 20, 20, bmp.Width, bmp.Height);

    // dispose of all objects that consume resources
    bmp.Dispose();
    gBmp.Dispose();
    redBrush.Dispose();
    greenBrush.Dispose();
}

The first thing to note here is that there are two Graphics objects here-gForm represents the form drawing surface, and gBmp represents the bitmap in memory. Next, note that we've set the CompositingMode property of the bitmap to SourceCopy-so we should expect that the green square (drawn second) should overwrite the red circle within the bitmap.

Finally, note that we have not specified a value for the CompositingMode property of the form drawing surface, gForm. Therefore, it will take the default value, which is SourceOver-and therefore, we should expect the circle and square to blend with the bars when we draw the bitmap image over the orange bars.

To confirm our expectations, let's run the example. The result is shown here. As expected, the square has overwritten the circle at the pixels where they overlap, but the square-and-circle combination has blended with the bars.

Click To expand

Getting and Setting Pixels

In previous chapters, we've talked about the importance of having control over the color and alpha of individual pixels on the drawing surface. For example, when creating 3D effects, just one pixel out of place can spoil the effect.

Pixel control is also useful when performing certain kinds of digital image manipulation. Consider, for example, a project I undertook recently in which I needed to remove graininess from very old black and white photographs (without excessively blurring the image). I used the following technique: first, to scan the negative with a very high quality scanner, good enough so that I could see the individual grains of the negative when looking at the image at high magnification. Then, apply an algorithm that locates the position and size of each photographic grain of the negative. Then, having located adjacent grains, calculate a geometric description of the boundaries between the grains. Finally, average the brightness of the pixels within one geometric shape that contains one photographic grain. In the later stages of this process, control of individual pixels was an important factor.

In GDI+, the Bitmap class provides methods for retrieving and setting the color (including the alpha value) of individual pixels. The following example demonstrates the technique, by creating a small image, drawing into the image, and then printing out the color values of each pixel in the image:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics gForm = e.Graphics;
    gForm.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create a bitmap in memory
    Bitmap bmp = new Bitmap(6, 6);
    Graphics gBmp = Graphics.FromImage(bmp);

    // Give the bitmap a white background
    gBmp.FillRectangle(Brushes.White, 0, 0, bmp.Width, bmp.Height);

    // Draw a red diagonal line  in the bitmap
    gBmp.DrawLine(Pens.Red, 0, 0, 5, 5);

    // Now draw the bitmap on our window
    gForm.DrawImage(bmp, 20, 20, bmp.Width, bmp.Height);

    // Finally, write the pixel information to the console window
    for (int y = 0; y < bmp.Height; ++y)
    {
        for (int x = 0; x < bmp.Width; ++x)
        {
            Color c = bmp.GetPixel(x, y);
            Console.Write("{0,2:x}{1,2:x}{2,2:x}{3,2:x}
                c.A, c.R, c.G, c.G);
        }
        Console.WriteLine();
    }
    bmp.Dispose();
}

When highly magnified, the image looks like this:

The output to the console is as follows:

ffff 0 0 ffffffff ffffffff ffffffff ffffffff ffffffff
ffffffff ffff 0 0 ffffffff ffffffff ffffffff ffffffff
ffffffff ffffffff ffff 0 0 ffffffff ffffffff ffffffff
ffffffff ffffffff ffffffff ffff 0 0 ffffffff ffffffff
ffffffff ffffffff ffffffff ffffffff ffff 0 0 ffffffff
ffffffff ffffffff ffffffff ffffffff ffffffff ffff 0 0

This output consists of the alpha-red-green-blue hexadecimal values for each of the 36 pixels in the image. These values are returned by the Bitmap object's GetPixel method. You can see that most of the pixels are pure white-they have a value of ffffffff (alpha=255, R=255, G=255, B=255). The pixels in the diagonal line are different-they have the value ffff 0 0 (alpha=255, R=255, G=0, B=0), and these pixels are red. We can see the direct relationship between the bitmap above, and the hexadecimal output.

Now try adding the following lines, to change the value of some of these pixels to green:

// create a green color and use it to change some pixels
Color green = Color.FromArgb(0×40, 0, 0×ff, 0);
for (int x = 0; x < bmp.Height; ++x)
{
   bmp.SetPixel(x, x, green);
}

Setting Alpha for an Entire Image

GDI+ supports facilities for applying color transformations (including the alpha component of colors). Color transformations allow us to create some impressive effects. For example, we might want to make the red component of all colors of an image brighter, or make the image transparent wherever there is blue in the image. We can use color transformations to achieve these types of effects.

The latter example cited above is similar to a technique that television weather forecasters have been using for years. We see them waving their hands over the map, showing the locations of highs, lows, thunderstorms, etc.-but in fact, they are standing in front of a blue wall in the TV studio, while a visual effect superimposes their image onto the computer-generated weather chart. In fact, these days many apparently elaborate television news studios only exist virtually. The news anchors sit at a desk in a blue room, and the designer uses software to create the illusion of an elaborately decorated, busy newsroom with computer monitors and people working in the background. The software can even simulate the optical depth of field of the television camera, so that items and people in the background are the appropriate level of blurriness. All of this makes it very easy to redecorate the newsroom!

Creating Semi-transparent Images

When building custom controls, we don't really need to involve a great deal of color transformations. However, one operation that is useful is that of making an image semi-transparent. To achieve this effect, we must use the ImageAttributes class, which allows us to modify the functionality of GDI+ and change how colors in an image are manipulated during rendering.

We also use the ColorMatrix class here. This class uses matrix algebra to help achieve the desired effect. A full explanation of matrix algebra is beyond the scope of this book. However, we'll include a brief explanation of the functionality of the ColorMatrix class as follows.

GDI+ represents the color of each pixel in a bitmap as a color vector with four elements (alpha, red, green, blue), and it happens that we could use a 4×4 matrix of numeric entries to apply linear transformations (such as color rotation and color scaling) to an image. However, the 4×4 matrix would not give us facilities for applying color translations. If we add a 'dummy' fifth column and row, we can use the fifth column for translation after doing a linear transformation.

The simplest color matrix is the identity matrix. If we perform a transformation by applying the identity matrix, the result is the same image we started with. In other words, the identity matrix doesn't change anything. For mathematical reasons that we won't go into here, an identity matrix is always a square matrix with 1s down the leading diagonal, and 0s elsewhere:

A good approach when trying out color transformations is to start with the identity matrix, then make small adjustments until we get the transformation that we desire.

Performing a transformation to set the alpha component for an entire image is very easy. If we leave all the other entries the same, we can change the entry in position [4] [4] in the matrix to scale the alpha component of a color. Each entry in the matrix is a single precision floating-point value, so to set the alpha component to 60% we use the following matrix:

Click To expand

Let's take a look at an example that uses this technique. The ColorMatrix and the ImageAttributes classes are in the System.Drawing.Imaging namespace, so include a using statement to that effect.

We'll draw a set of gray vertical lines on the drawing surface, using code similar to something we saw earlier in this chapter:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Bitmap bmp = new Bitmap("rama.jpg");
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Draw a set of gray vertical bars on the form
    for (int i = 1; i <= 7; ++i)
        Rectangle r = new Rectangle(i * 40−15, 0, 15,
            this.ClientRectangle.Height);
        g.FillRectangle(Brushes.Gray, r);
    }

Next, we'll define our color matrix. It's exactly the matrix we discussed above, which simply changes the alpha level to 60%:

// Create a color matrix
// The value 0.6 in row 4, column 4 specifies the alpha value
float[][] matrixItems = {
                            new float[] {1, 0, 0, 0, 0},
                            new float[] {0, 1, 0, 0, 0},
                            new float[] {0, 0, 1, 0, 0},
                            new float[] {0, 0, 0, 0.6f, 0},
                            new float[] (0, 0, 0, 0, 1}};
ColorMatrix colorMatrix = new ColorMatrix(matrixItems);

Now we'll use an ImageAttributes object (like the one we met earlier in the chapter). We assign our color matrix as a property of the ImageAttributes object:

// Create an ImageAttributes object and set its color matrix
ImageAttributes imageAtt = new ImageAttributes();
imageAtt.SetColorMatrix(
    colorMatrix,
    ColorMatrixFlag.Default,
    ColorAdjustType.Bitmap);

Finally, we use the ImageAttributes object to draw the image of Rama the dog:

// Now draw the semitransparent bitmap image.
g.DrawImage(
    bmp,
    this.ClientRectangle,  // destination rectangle
    0.0f,             // source rectangle x
    0.0f,             // source rectangle y
    bmp.Width,        // source rectangle width
    bmp.Height,       // source rectangle height
    GraphicsUnit.Pixel,
    imageAtt);

    imageAtt.Dispose();
}

In this example, the gray bars in the background help to show up the transparency of the image. When we run the example, it appears as follows:

Click To expand

Normally, we do this scaling when we draw an image to a drawing surface, but by creating a new bitmap, and drawing the source bitmap into the new bitmap while doing the color or alpha scaling, we can also use the facility to transform an image without drawing it to a window or a printer.


Playing an Animation

We've talked about displaying static images in our custom control; but GDI+ also provides a technique that allows us to display animated images in our custom control. Aside from the (debatable) niceties of having moving images, this has practical uses too: for example, if our custom control needs to perform some operation that will take quite a bit of time, we might display an simple animation to let the user know that the operation is proceeding properly.

In this example we demonstrate this tool using an animated GIF. This is simply a GIF file can contains number of images in a predefined format in such a way that they appear one after another and thus give the effect of animation. Each image corresponds to a frame of the animation. There are many freeware, shareware, and commercial tools that allow us to create GIF files that contain animations. The ImageAttribute class contains the functionality that plays an animation from a GIF file.

This example is slightly more complex than the others in this chapter. First, we need to create a Form-level variable to represent our animation:

public class Form1 : System.Windows.Forms.Form
{
    private Bitmap bmp;
    ...

Next, we need to add a Load event handler to the form. Modify it as follows:

private void Form1_Load(object sender, System.EventArgs e)
{
    bmp = new Bitmap("arrow.gif");
    ImageAnimator.Animate(bmp, new EventHandler(this.OnFrameChanged));
}

If you do this manually, you'll need to add this line to InitializeComponent():

private void InitializeComponent()
{
    ...
    this.Load += new System.EventHandler(this.Form1_Load);
}

Next, we need a Paint event handler:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    //Get the next frame ready for rendering.
    ImageAnimator.UpdateFrames();
    //Draw the next frame in the animation.
    e.Graphics.DrawImage(this.bmp, new Point(0, 0));
}

Finally, add this private member method, which is used in the Paint event handler:

private void OnFrameChanged(object o, EventArgs e)
{
    // Invalidate the window to force a call to the Paint event handler.
    // If we had more items in the window than just the animation, we could
    // invalidate just the area occupied by the animation,
    this.Invalidate();
}

For this example to work, the file that we load must be a GIF animation. We've provided a GIF animation called arrow.gif-if you're running this in debug mode, place arrow.gif in the /bin/debug subdirectory of your application directory. When we run the example, the animation will play.

Click To expand


Converting a BMP to a JPEG

Some file formats are more appropriate for certain uses than others. For example, the JPEG format is a good format to use when compressing photographs. The GIF format is appropriate where we have computer-generated images that have rectangular regions that are a uniform color. The BMP format may be used by some legacy software programs and may be used for compatibility. If we have a BMP file that contains a photograph, its footprint (that is, the disk-space it consumes) will be quite large in comparison to the same photograph stored as a JPEG. So, we may want to convert the BMP file to JPEG format.

GDI+ allows us to perform this kind of file conversion programmatically. If we want to do a simple BMP-to-JPEG file conversion, we can do it as follows:

Bitmap bmp = new Bitmap("GrapeBunch.bmp");
bmp.Save("GrapeBunch.jpg", System.Drawing.Imaging.imageFormat.Jpeg);

Controlling the Compression Level

The code above assumes the default levels of compression and image quality in the resulting JPEG, but with a little more code, we can take control of these factors within the application. If we want to do this extra work, then we need to follow these steps.

Before we start, we need to be able to get the image encoder/decoder information for our desired target image type. To achieve this step, we'll create a utility method, GetEncoderInfo, to get the encoder:

private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
    int j;
    ImageCodecInfo[] encoders;
    encoders = imageCodecInfo.GetImageEncoders();
    for(j = 0; j < encoders.Length; ++j)
        if(encoders[j].MimeType == mimeType)
            return encoders[j];
    return null;
}

This uses the GetImageEncoders method (which we met earlier in this chapter) to get the entire list of codecs; then we just search through them looking for the right one.

OK, now we can start the conversion process. We need a using statement for the System.Drawing.Imaging namespace:

using System.Drawing.Imaging;

We use the utility method above to get the correct codec, and we assign it to the variable ici. Then we declare a variable called enc, of type Encoder, and set it to Encoder.Quality.We also declare a variable called epa, of type EncoderParameter, and initialize it with the desired level of compression:

ImageCodecInfo ici;
Encoder enc;
EncoderParameter ep;
EncoderParameters epa;

// Initialize the necessary objects
ici = GetEncoderInfo("image/jpeg");
enc = Encoder.Quality;
epa = new EncoderParameters(1);

When we perform the Save operation itself, we will need to pass an array of EncoderParameter objects. So we declare an array of length 1, and initialize the first element in the array to our initialized EncoderParameter object:

// Set the compression level
ep = new EncoderParameter(enc, 25L);
epa.Param[0] = ep;

The second argument of the EncoderParameter constructor indicates the quality level. We've specified that we want a 25% quality level. This argument must be a value between 0 and 100. The level of compression increases as the quality level reduces.

Finally, we can load the BMP file and call the Bitmap.Save method, passing the ImageCodecInfo and the array of EncoderParameter objects as arguments:

// create a Bitmap object from a BMP file.
Bitmap bmp = new Bitmap("GrapeBunch.bmp");

// Convert to JPEG and Save
bmp.Save("GrapeBunch.jpg", ici, epa);

The code above provides an example of what we need to do if we are doing an advanced operation in GDI+ that requires a codec and encoder parameters. Some of the operations require more than one encoder parameter. In this case, we can construct the EncoderParameters passing the required number of encoder parameters, and set the Param property as appropriate.


Summary

Whether we're writing an application that is image driven, or just putting together a simple application that would be enhanced by a few images, we've seen in this chapter how GDI+ gives us control over the use of images in our applications. When we're working with images, we're generally trying to do one of four things-loading or creating an image, displaying or rendering an image, manipulating an image, or saving the image. In this chapter we saw how GDI+'s Image class-and the Bitmap and Metafile classes that inherit from it-provide us with this functionality.

We focused in particular on the Bitmap class, which is used to represent a raster-based image in memory, and is therefore of particular relevance. We saw how we can use a Bitmap class constructor to load an image from a file on disk (such as a BMP, JPEG, GIF, or PNG), and hence create a Bitmap object that represents a copy of the image in memory. We also saw how we can use other Bitmap class constructors to create a brand new, empty image in memory-like a blank canvas onto which we can apply drawing operations and import portions of other images.

In order to display an image on our drawing surface, we can use the Graphics.DrawImage method. As we saw, it's worth thinking about checking the size and resolution of the image, and the resolution of the drawing surface, when thinking about how big you want the image to appear on the drawing surface. It's also possible to override all that by declaring an explicit rectangle (or the client rectangle) in which you want the image to be drawn. We saw how we can scale an image, or change its size, or its resolution-all these issues affect the quality of the displayed image.

We saw just a few of the many different ways we might want to manipulate an image-by cropping it, or skewing, reflecting, or rotating it, or cloning it. We also saw how to get thumbnail information from an image.

We saw how a technique called double-buffering can be used to improve performance, particularly in a situation where we're drawing complex images at run time. It takes advantage of the layering effect to reduce the amount of work that needs to be done, and make the whole drawing task more manageable (by breaking the image down into related groups of elements), and thus also improves the rendering (particularly on lower-spec machines). We also revisited the subject of alpha here-particularly as it relates to layering one image over another.

We saw a few other useful techniques too: we used the Graphics object's Draw… methods to control the color and alpha of individual pixels in the image, and saw a handy technique for working with animated images (such as animated GIFs). We used the Bitmap.Save method to save a file to a specified image format (and we saw, in the process, how it allows us to convert an image from one format to another).

Aside from these very practical topics, we also noted a number of different image formats, making observations about how they store the information (particularly in relation to the color depth, compression format, and pixel format). There are plenty of different formats to choose from, and our choice affects the quality of the stored or displayed image. We also saw how we can check for support of certain image formats-by using the ImageCodecInfo class to confirm the existence of the appropriate codec on the system.

We've covered most of the image-related functionality you should need when you're creating custom controls. In the next chapter, we're going to turn our attention to the subject of graphics paths and regions.


Chapter 6: GraphicsPaths and Regions

Overview

Back in the early chapters of this book, we used pens and brushes (in conjunction with the Draw…and Fill…methods of the Graphics object) to create abstract regular shapes on the page. We drew and filled lines, rectangles, regular curves, and ellipses.

In this chapter we'll take a look at two classes that take this a significant step further: the GraphicsPath and the Region. These classes enable us to work with more irregular shapes, and to group sets of shapes together for more effective processing.

So, in this short chapter we will cover:

  • How we can use the GraphicsPath class to describe paths in GDI+

  • How we can describe regions, using the Region class

  • The similarities, differences, and relationships between GraphicsPaths and Regions

This chapter covers the basic features of GraphicsPaths and Regions-specifically, we focus on how we use these classes to compose outlines and shapes and perform some simple operations with them. In the next chapter, we'll see how these objects can be used in operations such as clipping, invalidation, and creating custom gradient brushes, to create some very useful effects.


What are GraphicsPaths and Regions?

The GraphicsPath and Region are two classes that contribute significantly to the power of GDI+ as a graphics-programming interface. These two classes are intimately related, as we'll see in this section, and yet quite distinct in their purposes and capabilities. Together, they allow us to build higher levels of abstraction, and thereby create simpler, cleaner code.

A GraphicsPath object represents a set of sub-paths or figures. Each figure is a connected path, composed of a series of line and curve segments and geometric figures. These segments and geometric figures can be described in any of the standard ways we've seen already in this book-we can use arcs, Bézier curves, ellipses, rectangles, and so on. To create a figure, we simply list the components of the figure in the right order-the GraphicsPath object computes the figure to be the outline that results when these component outlines are joined together in the order specified. (We'll see some examples of this in the next section.)

The GraphicsPath itself is the path composed of the ordered set of figures. Because the set of figures is ordered, and because each figure is composed of an ordered set of lines and curves, the GraphicsPath itself is a path with a beginning and an end.

The figures within a GraphicsPath object don't need to be connected to one another-they can be disconnected. In particular, this means that the figures don't have to be top-to-tail on the drawing surface: in fact, usually, they're not top-to-tail and thus the path described by the GraphicsPath object is discontinuous. To follow such a path from one end to the other, we would need to ‘leap through space' from the end of one figure to the beginning of the next.

The Region class is similar to the GraphicsPath path class, but it relates to areas (or regions) instead of paths. Thus, we can use a Region object to represent a shape, or a set of shapes. A region can consist of geometric shapes (like rectangles and ellipses) and/or custom shapes whose outlines are composed of whatever lines and curves we care to specify. Like a GraphicsPath, the shapes that make up a Region don't necessarily have to be connected.

Once we've created and defined a GraphicsPath object, we can draw it to our drawing surface using a Pen object. After constructing a Region object, we can fill the region using a Brush object. In both cases, there's an important point here. We can use the GraphicsPath and Region classes to compose complex paths and shapes; but the task of rendering the completed path or shape to the screen can be achieved in a single method call This makes the rendering process much tidier, and also allows us to organize our code more tidily.

Let's start to look at some real GraphicsPaths and Regions, so we can get a better idea of how they work.

The GraphicsPath Class

The GraphicsPath class encapsulates a connected series of lines and curves. You may recall that we used a GraphicsPath object back in Chapter 3, to demonstrate the effect created by a PathGradientBrush.

Essentially, we use a GraphicsPath object when we want to describe an outline. Once we've described the outline, we can do a number of things with it: we can draw it using a pen, or fill its interior using a brush, or use it to create clipping regions.

We'll see all these applications of our outline during this chapter and the next. Before we get to that, we should look at some simpler examples and get the basics under our belt.

Composing a GraphicsPath

As we noted earlier in the section, before we can do anything with a GraphicsPath path, we need to compose it. A GraphicsPath is composed of an ordered set of figures; and each figure is composed of an ordered set of line and curve segments, and geometric figures.

Once we've created a new GraphicsPath object, we compose the GraphicsPath path segment by segment, and figure by figure. The order in which we specify the segments and figures defines the order and direction of the path-and hence the start and end points of the path. Let's look at an example:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create a new GraphicsPath object
    GraphicsPath gp = new GraphicsPath();

    // Create a figure
    gp.AddLine(10, 10, 10, 50);
    gp.AddBezier(10, 50, 10, 55, 25, 70, 30, 70);
    gp.AddLine(30, 70, 60, 70);
    gp.AddBezier(60, 70, 85, 70, 90, 55, 90, 50);
    gp.AddLine(90, 50, 90, 30);
    gp.AddLine(90, 30, 120, 10);
    gp,AddLine(120, 10, 150, 10);
    gp.AddLine(150, 10, 170, 30);
    gp.AddLine(170, 30, 170, 70);

    // Create another figure
    gp.StartFigure();
    gp.AddLine(60, 110, 40, 160);
    gp.AddLine(40, 160, 60, 180);
    gp.AddLine(60, 180, 140, 150);
    gp.AddLine(140, 150, 120, 110);

    // draw the path
    g.DrawPath(Pens.Black, gp);

    // Clean up
    gp.Dispose();
}

If you'e trying these examples for yourself, remember that the GraphicsPath class is part of the System.Drawing.Drawing2D namespace, so you'll need a using directive to include it.

In this code we've created a path that contains two figures. We've composed the first figure by adding a series of line and Bézier curve segments to the GraphicsPath object, using the AddLine and AddBezier methods. It begins with a line-segment from the point (10, 10) to (10, 50), and then continues with a Bézier curve round to (30, 70), followed by another line segment, another Bézier curve, and then five more line segments-ending on the point (170, 70).

When we call the StartFigure method, we begin a second figure. By definition, this means that we have finished composing the first figure. As you can see, the second figure is composed of four lines, beginning at (60, 110) and ending at (120, 110).

The whole path looks like this. Its start point is the point (10, 10), and its end point is the point (120, 110). It's not a continuous line-in order to follow it from one end to the other we must jump from the end of the first figure to the start of the second.

Click To expand

Note that the GraphicsPath class itself doesn't provide any functionality for rendering the path we've created. If we want to render the path contained in a GraphicsPath object, then we call a method in the Graphics class-and pass the GraphicsPath object as an argument. (In fact, as we'll see later, the Region object is very similar in this respect.)

Drawing Multiple Figures in a GraphicsPath

In the example above, note how we composed the two figures. To begin describing the first figure, we just launched into a sequence of Add… methods, which sequentially build up the first figure. We must finish describing the first figure before we begin describing the second figure-because as soon as we call StartFigure to begin describing the second figure, GDI+ takes it as read that you've finished describing the first figure. There's no going back to it!

We can call StartFigure as often as we like. Each time we call it, it will end the existing figure and begin a new one.

Open Figures and Closed Figures

A figure is closed if its start point and end point are the same. If a figure is not closed, then we say that it's open. (It's just like a pen-if you don't close the pen, the sheep can get out.) In the example above, the first figure starts at (10, 10) and ends at (170, 70), and is therefore open. The second figure starts at (60, 110) and ends at (120, 110), and so is also open.

There are a number of ways we can close a figure. The obvious way is explicitly to add a line or curve segment at the end of the figure, whose end point is the same as the start point of the figure.

An easier way is to use the CloseFigure method when we've finished describing the figure, like this:

// Create a new GraphicsPath object
GraphicsPath gp = new GraphicsPath();

// Create a figure
gp.AddLine(10, 10, 10, 50);
gp.AddBezier(10, 50, 10, 55, 25, 70, 30, 70);
gp.AddLine(30, 70, 60, 70);
// Close this figure
gp.CloseFigure();

// Create another figure
gp.StartFigure();
gp.AddLine(60, 110, 40, 160);
gp.AddLine(40, 160, 60, 180);
gp.AddLine(60, 180, 140, 150);
gp.AddLine(140, 150, 120, 110);

// draw the path
g.DrawPath(Pens.Black, gp);

// Clean up
gp.Dispose();

Now, the first figure is composed of a line from (10, 10) to (10, 50); a Bézier curve to (30, 70); a line to (60, 70); and a line that closes the figure by joining this last point back to the first. In fact, the CloseFigure method not only closes the existing figure; it also starts the next figure automatically, so that the call to StartFigure here (for the second figure) is in fact superfluous.

Click To expand

Alternatively, we can call the CloseAllFigures method, which closes all the figures in the path upto and including the current one:

// Create a new GraphicsPath object
GraphicsPath gp = new GraphicsPath();

// Create a figure
gp.AddLine(10, 10, 10, 50);
gp.AddBezier(10, 50, 10, 55, 25, 70, 30, 70);
gp.AddLine(30, 70, 60, 70);

// Create another figure
gp.StartFigure();
gp.AddLine(60, 110, 40, 160);
gp.AddLine(40, 160, 60, 180);
gp.AddLine(60, 180, 140, 150);
gp.AddLine(140, 150, 120, 110);

// Close all figures
gp.CloseAllFigures();

// draw the path
g.DrawPath(Pens.Black, gp);

// Clean up
gp.Dispose();

This code creates two open figures, and then closes them both off at the same time. Finally, it draws the two closed figures to the drawing surface.

Click To expand

We can also add geometric figures to the path. By nature, these figures are already closed, so the CloseFigure and CloseAllFigures methods have no effect on these figures. For example:

// Create a new GraphicsPath object
GraphicsPath gp = new GraphicsPath();

// Create a figure
gp.AddRectangle(new Rectangle(10, 50, 80, 20));

// Create another figure
gp.AddEllipse(50, 10, 20, 80);

// draw the path
g.DrawPath(Pens.Black, gp);

// Clean up
gp.Dispose();

In this example, the first figure is a rectangle and the second is an ellipse. Note that there's no need to use StartFigure to end the first figure and start the second, because the first figure is already closed (and therefore complete).

Click To expand

In short: if the first and last points of a figure are different, and we don't call the CloseFigure method for the figure, and we don't call the CloseAllFigures method, then the figure will be open.

Filling the Area Bounded by a GraphicsPath

As we'll see a little later, we can use a GraphicsPath as the boundary of a Region-but only if all the figures in the GraphicsPath are closed.

Moreover, we can apply a Brush object to fill the area bounded by our GraphicsPath object, like this:

GraphicsPath gp = new GraphicsPath();

// create an open figure
gp.AddLine(10, 10, 10, 50);
gp.AddLine(10, 50, 50, 50);
gp.AddLine(50, 50, 50, 10);

// start a new figure
gp.StartFigure();
gp.AddLine(60, 10, 60, 50);
gp.AddLine(60, 50, 100, 50);
gp.AddLine(100, 50, 100, 10);
gp.CloseFigure();

// add a geometric shape (a rectangle) to the path
Rectangle r = new Rectangle(110, 10, 40, 40);
gp.AddEllipse(r);

// Fill the area ‘bounded' by the path
g.Fi11Path(Brushes.Orange, gp);

// Draw the path
g.DrawPath(Pens.Black, gp);

// Clean up
gp.Dispose();

Of course, it doesn't make sense to fill an open area, like the one we've created here. In this case, the FillPath method works out the closed path that we would get if we called CloseAllFigures on our GraphicsPath. Then it uses the specified Brush to fill the area bounded by that closed path:

Click To expand

As you can see, GDI+ doesn't actually call CloseAllFigures on the GraphicsPath itself. It calls CloseAllFigures on an implicit copy of our GraphicsPath, and uses that path to perform the filling operation.

Absolute Coordinates and a Shorthand for Straight-line Figures

Note that the arguments to the GraphicsPath.AddLine method (and other Add… methods) are not relative-they represent the absolute coordinates of the start point and end point. Thus, the operation gp.AddLine(50, 0, 0, 20) will add a line that runs between the points (50, 0) and (0, 20)-regardless of what we've added to the figure so far.

This raises the interesting question of what happens when consecutive Add… method calls describe line or curve fragments that don't top-and-tail one another. For example, in the first example we created a 9-segment figure like this:

// Create a figure
gp.AddLine(10, 10, 10, 50);
gp.AddBezier(10, 50, 10, 55, 25, 70, 30, 70);
gp.AddLine(30, 70, 60, 70);
gp.AddBezier(60, 70, 85, 70, 90, 55, 90, 50);
gp.AddLine(90, 50, 90, 30);
gp.AddLine(90, 30, 120, 10);
gp.AddLine(120, 10, 150, 10);
gp.AddLine(150, 10, 170, 30);
gp.AddLine(170, 30, 170, 70);

Here, we've carefully specified the arguments of these Add… methods so that each one runs on from the previous one-so that segments n and n+1 meet at (10, 50), (30, 70), (60, 70), (90, 50), (90, 30), (120, 10), etc. What happens if we're not so careful?

In fact, if the end of one segment is not the same as the beginning of the next segment, then GDI+ assumes that you want to join them with a straight line segment. What this means is that, if we wish, we can specify figures with straight-line segments by omitting some of the AddLine operations (just so long as we don't omit any of the coordinate information).

For example, we can describe the above figure with three fewer AddLine operations than we've shown above:

   // Create a figure
   gp.AddLine(10, 10, 10, 50);
   gp.AddBezier(10, 50, 10, 55, 25, 70, 30, 70);
// gp.AddLine(30, 70, 60, 70);
   gp.AddBezier(60, 70, 85, 70, 90, 55, 90, 50);
   gp.AddLine(90, 50, 90, 30);
// gp.AddLine(90, 30, 120, 10);
   gp.AddLine(120, 10, 150, 10);
// gp.AddLine(150, 10, 170, 30);
   gp.AddLine(170, 30, 170, 70);

GraphicsPath Properties and Methods

There are a number of methods and properties that give us quite a bit of power when working with paths. The following table gives an overview of the public properties of the GraphicsPath class, of which there are five:

Property

Description

FillMode

If we're filling the interior of the path, then this property dictates how it will be filled. Its values come from the FillMode enumeration (which has two values-Alternate and Winding). We can get or set the value of this property. See the additional note below.

PathData

This gets a PathData object that contains information on how the path is constructed. (The PathData object has two properties-Points and Types-which give the same arrays as those described in the PathPoints and PathTypes properties in this table.)

PathPoints

This gets the points in the path, in the form of an array.

PathTypes

Each point in a GraphicsPath has a type (for example, Start, Line, Bezier, CloseSubpath). This property gets the types of the points in the path, in the form of an array.

Note that all the possible point types are listed in the PathPointType enumeration, which is part of the System.Drawing.Drawing2D enumeration.

PointCount

This gets the number of elements in the arrays returned by the PathPoints or PathTypes properties.

As we state in the table above, there are two possible values for the FillMode property-Alternate, and Winding. The exact way that these fill modes fill the interior of a path can depend on the orientation of the path-that is, the start and end points of the path and the resulting direction of the path.

For the purposes of writing custom controls, the use of intricate fill patterns is likely to be minimal, so we won't dwell on the details of this point here. However, it's worth noting that the fill behavior of some brushes can depend on the orientation of the path-and therefore can depend on the order in which you describe the individual line and curve segments and the individual figures.

Most of the methods in the GraphicsPath class facilitate construction of our path. These methods, which are all prefixed with the word Add…, allow us to add things like lines, curves, and geometrical figures to the path. We've already covered this process in some detail, so we will not tackle it further here, except to say that the full list of these methods includes:

AddArc | AddClosedCurve | AddLine | AddPie | AddRectangles
AddBezier | AddCurve | AddLines | AddPolygon | AddString
AddBeziers | AddEllipse | AddPath | AddRectangle |

The following table lists some of the other methods of the GraphicsPath class. This table is not intended to be a reference, but rather is a guide to the different ways that we can construct a path.

Method | Description
ClearMarkers | See below.
Clone | Creates a GraphicsPath object that contains a copy of the current path.
CloseFigure | Closes the current figure by adding a line between the ending point of the last segment and the starting point of the first segment. Automatically starts a new figure in the path.
CloseAllFigures | Closes all figures drawn so far, by adding lines between each figure's end point and its start point. Automatically starts a new figure in the path.
Flatten | Converts curves into a series of connected line segments.
GetBounds | Returns a rectangle that encloses the path.
IsOutlineVisible | Determines whether a particular point is contained in the outline of the path. Could be used for hit testing.
IsVisible | Determines whether a particular point is contained in the interior of the path.
Reset | Clears the path.
Reverse | Reverses the order of the points in the path.
SetMarkers | See below.
StartFigure | Starts a new figure in the path. Leaves the previous figure open.
Transform | Applies a transform matrix to the path.
Warp | Applies a warp transform to the path. A warp transform is defined by a rectangle and a parallelogram.

The SetMarkers and ClearMarkers methods relate (unsurprisingly) to a feature of GDI+ GraphicsPaths, called the marker. We can use SetMarkers to insert a marker at a specific point in a path as we are constructing a path. Later, we can create a new path that consists of a subset of the original path that lies between two markers in that path. We can use the ClearMarkers method to clear any markers from an existing path. We use markers to separate groups of sub-paths.

Finally, it's worth noting that the GraphicsPath object constructor is overloaded to accept an array of points, and/or an array of point-types, and/or a fill mode. If our GraphicsPath isn't very complicated, this approach works well. See the documentation at http://msdn.microsoft.com/library for more information.

The Region Class

As we've said, we can use a Region object to describe a region of our surface-that is, zero or more areas of real estate on our surface. When we work with a Region object, we generally begin by defining what area the Region covers, and then performing operations that use the defined region. (In this way, at least, we use a Region object in much the same way as we'd use a GraphicsPath object.) Once we've defined our region, we can perform a number of operations: we can fill it with a Brush object, or use it for hit testing or as a clipping region (see the next chapter).

It makes sense to begin by seeing how we might define a Region. Unlike the GraphicsPath class, the Region class doesn't have an army of Add…methods. In fact, while a GraphicsPath object has its figures, there is no equivalent concept in a Region object-while a Region object typically does consist of a number of disconnected enclosed areas, they're created in a very different way. In particular, we don't build up a Region incrementally, like we did a GraphicsPath.

Instead, we usually create a Region by using one of the overloaded Region class constructors. We'll look at some ways to create Regions here.

Creating a Region from a GraphicsPath

One way to create a Region is to use a GraphicsPath to define a path, and then use this path as the outline of our new Region object.

In this example, we'll use a familiar GraphicsPath object (one from earlier in the chapter) to create a region, and then we'll fill the resulting region with a Brush:

// Create a GraphicsPath
GraphicsPath gp = new GraphicsPath();

// create an open figure
gp.AddLine(10, 10, 10, 50);
gp.AddLine(10, 50, 50, 50);
gp.AddLine(50, 50, 50, 10);

// start a new figure
gp.StartFigure();
gp.AddLine(60, 10, 60, 50);
gp.AddLine(60, 50, 100, 50);
gp.AddLine(100, 50, 100, 10);
gp.CloseFigure();

// add a geometric shape (a rectangle) to the path
Rectangle r = new Rectangle(110, 10, 40, 40);
gp.AddEllipse(r);

// Create a Region whose boundary is the above GraphicsPath
Region reg = new Region(gp);

// Fill the Region
g.FillRegion(Brushes.Green, reg);

// Clean up
reg.Dispose();
gp.Dispose();

Here's the result. The shapes that we see are familiar, but the way we've filled them is different. The Region class constructor we've used here is one that accepts a GraphicsPath object; it takes a copy of the path, closes it, and then uses it to create a Region. As you can see, this region is composed of three disconnected areas: two 4-sided polygons and a circular ellipse.

Click To expand

So, Regions (like GraphicsPaths) don't need to be connected or contiguous. Moreover, GraphicsPaths give us a way to describe regions whose boundaries are irregular. For example:

// Create a GraphicsPath
GraphicsPath gp = new GraphicsPath();

// Create a figure
gp.AddLine(10, 10, 10, 50);
gp.AddBezier(10, 50, 10, 55, 25, 70, 30, 70);
gp.AddLine(30, 70, 60, 70);
gp.AddBezier(60, 70, 85, 70, 90, 55, 90, 50);
gp.AddLine(90, 50, 90, 30);
gp.AddLine(90, 30, 120, 10);
gp.AddLine(120, 10, 150, 10);
gp.AddLine(150, 10, 170, 30);
gp.AddLine(170, 30, 170, 70);

// Create another figure
gp.StartFigure();
gp.AddLine(60, 110, 40, 160);
gp.AddLine(40, 160, 60, 180);
gp.AddLine(60, 180, 140, 150);
gp.AddLine(140, 150, 120, 110);

// Create a Region whose boundary is the above GraphicsPath
Region reg = new Region(gp);

// Fill the Region
g.FillRegion(Brushes.Green, reg);

// Clean up
reg.Dispose();
gp.Dispose();

Here's another region whose outline is a familiar shape (it's outline is the GraphicsPath we drew at the beginning of this chapter). This time, the top figure is not entirely concave, so when GDI+ closes the figure in order to create the region, the resulting interior is in two pieces.

Click To expand

Creating a Region from a Rectangle

We can create a Region from a Rectangle object, in a very similar way:

// Create a Region whose boundary is the above Rectangle
Region reg = new Region(new Rectangle(10, 10, 80, 20));

// Fill the Region
g.FillRegion(Brushes.Green, reg);

// Clean up
reg.Dispose();

The result isn't very surprising; but it's handy to have an easy way to put together a rectangular Region, because many of the regions you're likely to need will be rectangular.

Click To expand

Creating a Region from Another Region

To create a Region from an existing region, the constructor doesn't accept the existing region object itself-instead it expects an array of data about the region. We can get the required data about such a Region from its GetRegionData method-this method returns the required data in the form of a RegionData object. We can then pass this object into the Region constructor, and hence create a new Region object just like the existing one:

Region r1 = new Region(new Rectangle(10, 10, 80, 20));
RegionData r1Data = r1.GetRegionData();
Region r2 = new Region(r1Data);

Alternatively, it's easier just to use the Clone method instead:

Region r1 = new Region(new Rectangle(10, 10, 80, 20));
Region r2 = r1.Clone();

Unions and Intersections of Regions

Once we've constructed a region, we can modify it. The Region object provides five methods that allow us to perform set algebra on the region. This involves specifying a second area (in the form of a Rectangle, a RectangleF, another Region, or the internal of a GraphicsPath), and performing an operation-this results in a new Region that is a subset of the two.

Here's an example. Here, we first set the Region object to represent the area inside the tall rectangle. Then we modify it-we reduce its size by taking only the intersection of the Region with the region described by rect2:

// Create two rectangles
Rectangle rect1 = new Rectangle(50, 10, 50, 130);
Rectangle rect2 = new Rectangle(10, 50, 130, 50);

// Set Region using first rectangle
Region reg = new Region(rect1);

// Modify Region to be intersection of itself with second rectangle
reg.Intersect(rect2);

// Draw the result so we can see it
g.FillRegion(Brushes.Orange, reg);
g.DrawRectangle(Pens.Black, rect1);
g.DrawRectangle(Pens,Black, rect2);

The resulting Region is shown in the shaded area here. The screenshot also shows the two rectangles.

Click To expand

All five methods work in a very similar way-they represent five different set algebra operations. The methods are listed below:

Operation | Description
r1.Intersect(r2); | Updates r1 to be the intersection between r1 and r2 (that is, any portion that is in both r1 and r2)
r1.Union(r2); | Updates r1 to be the union of r1 and r2 (that is, any portion that is in either r1 or r2 or both)
r1.Xor(r2); | Updates r1 to be the exclusive union of r1 and r2 (that is, any portion that is in either r1 or r2 but not both)
r1.Complement(r2); | Updates r1 so that it includes areas that are contained within r2, but excludes any portion that was originally contained in r1
r1.Exclude(r2); | Updates r1 to exclude any portion that is also contained within r2

Here's a screenshot that demonstrates all five. The code is quite similar to the code we've just seen, so we'll omit it here-though it is included within the support code for the book. In each of these, r1 is the tall thin rectangle and r2 is the short wide one:

Click To expand

Methods of the Region Class

There are a number of other methods that we can use to manipulate regions and query regions (and return information about them). Some of them are listed below. For a full list, see the documentation at http://msdn.microsoft.com/library. We will not cover them all in this chapter, but you will see some of them in use elsewhere in the book:

Method | Description
Clone | Returns a Region object that is a copy of the existing one.
GetBounds | Gets a Rectangle object that bounds this Region object on the specified drawing surface.
GetRegionData | Gets a RegionData object that contains data that defines the region. We can use the RegionData object to create a new Region.
GetRegionScans | Gets an array of RectangleF objects that approximate this Region.
IsEmpty | Returns a Boolean-true if the Region is empty on the specified drawing surface, false otherwise.
IsInfinite | Returns a Boolean-true if the Region has an infinite interior, false otherwise. See below.
IsVisible | Returns a Boolean-true if any part of the Region is visible when drawn on the drawing surface of the specified Graphics object; false otherwise. There are a number of overloads for this method, that test for visibility of an intersection of the Region with a given point or rectangle on the given drawing surface.
MakeEmpty | Initializes this Region so that its interior is empty.
MakeInfinite | Initializes this Region so that its interior is infinite. See below.
Transform | Applies a transform matrix to the Region.
Translate | Offsets the Region by an X and Y offset.

Let's finish off this section with a couple of examples that demonstrate some of these methods. We can illustrate the methods that return Booleans as follows:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create a Region object, cut a rectangular hole in it, and fill it
    Region r = new Region(new Rectangle(30, 30, 30, 60));
    r.Exclude(new Rectangle(40, 40, 10, 10));
    g.FillRegion(Brushes.Orange, r);

    // Tell us about the Region
    Console.WriteLine("This Region: ");
    Console.WriteLine(r.IsInfinite(g) ? " - is infinite"
                                      : " - is finite");
    Console.WriteLine(r.IsEmpty(g) ? " - is empty"
                                   : " - is non-empty");

    PointF pf = new PointF(35.0f, 30.0f);
    Console.WriteLine((r.IsVisible(pf) ? " - includes"
                                       : " - excludes")
                      + " the point (35.0, 50.0)" );

    Rectangle rect = new Rectangle(25, 65, 15, 15);
    g.DrawRectangle(Pens.Black, rect);
    Console.WriteLine((r.IsVisible(rect) ? " - is visible"
                                         ; " - is invisible")
                      + " in the rectangle shown" );

    r.Dispose();
}

When we run this, we get a list of output to the console window. We also see the region in the client window.

This Region:
 - is finite
 - is non-empty
 - includes the point (35.0, 50.0)
 - is visible in the rectangle shown
Click To expand

As you can see, the filled region is finite, in that it doesn't extend in all directions as we see it in the drawing surface. It's also non-empty-it contains at least one point. We've selected the point (35, 50) to see whether it's contained within the region-and the output tells us that it is. Finally, the output confirms that the region is visible through the rectangle shown-because the interior of the rectangle contains a part of the region.

Infinite Regions

It's worth a note on infinite regions. It's sometimes useful to set a region to be equal to the entire drawing surface-extending infinitely in both directions. We then can use the set algebra operations (Intersect, Union, Xor, Compliment, and Exclude) to cut out bits of the infinite region and get our desired region.

This is useful, for example, we want to draw a background over the entire custom control, apart from certain rectangles or regions in which we want to draw other elements. In this case, making an infinite region (using the MakeInfinite method) and cutting rectangles out of it is a very convenient approach:

// Create a Region object and set it to be infinite
Region r = new Region();
r.MakeInfinite();

// Now cut a rectangular hole in it, and draw the result
r.Exclude(new Rectangle(30, 30, 50, 20));
r.Exclude(new Rectangle(30, 60, 50, 20));
g.FillRegion(Brushes.Orange, r);

r.Dispose();

Here, we've set the Region object to be the infinite space, before cutting out a couple of rectangles and filling what's left.

Click To expand


Summary

In this chapter, we covered the GraphicsPath and Region classes. A GraphicsPath object is used to represent a path. This path is composed of an ordered set of figures, and each figure is composed of an ordered set of line segments, curve segments, and geometric figures. To define the GraphicsPath object, we build up the path piece by piece. A Region object, by contrast, represents an area. We can't define a Region piece by piece-more commonly, we describe a Region by using set algebra operations (intersect, Union, Xor, Compliment, and Exclude).

GraphicsPaths and Regions can both cope with being disconnected or discontinuous, and a Region can be infinite.

We saw how each of the figures in a GraphicsPath is either open or closed, and that we can call the CloseFigure method or CloseAllFigures method to close open figures. We can use a GraphicsPath as the outline of a Region-if the GraphicsPath contains any open figures, then GDI+ works out what the closed path would look like and then builds the Region from that.

We can draw a GraphicsPath's path using a Pen object, and we can fill a Region using a Brush object. (As a sort of shorthand, we can also fill the region whose boundary is a given GraphicsPath directly, by passing the GraphicsPath and a Brush to the Graphics.FillPath method.)

Both the GraphicsPath and Region classes have many utility methods and properties that give us a great deal of power and flexibility.

In the next chapter, we'll see how the GraphicsPath and Region classes can be very useful in the context of clipping. We're also going to take a look at invalidation.


Chapter 7: Clipping and Invalidation

Overview

In the previous chapter, we looked at how we can use a Region object to describe a portion of our graphic. This portion or region may consist of a single 'fenced-off' area, or a union of several disconnected areas. We saw that we can describe region using a Region object directly, or we can describe it indirectly by using a GraphicsPath object to describe the outline of the region. Regions are quite versatile, and we can apply them in a wide variety of ways.

In this chapter, we see some direct applications of the concept of a region. For example, we can use regions to restrict the area of a drawing surface that our drawing operations can be applied to (and hence force the rest of the surface to remain unaffected). This technique allows us to draw one region of the graphic at a time, and to be very selective about what goes into that region. It's useful, for example, when we're representing a grid-structured graphic (like a spreadsheet) in which the contents of some of the grid cells may be too big to fit into the cell.

We can also use a region to control (and potentially to reduce) the amount of work that the operating system needs to do when it renders our graphic to the screen. If the size of the graphic is considerably larger than the client window, then we can use the relative sizes of each to arrange that the OS draws only the visible parts of our graphic. Moreover, when the window-size changes and the graphic needs redrawing, the amount of redrawing can be limited. We'll see examples of this too.

We'll also take a look at what happens when our custom control, having been rendered to the screen, is then partially or totally obscured by another window. If that second window is then moved, our control will need to be partially or totally redrawn on the screen. Again, the OS can limit the amount of re-rendering work it has to do, by calculating what region of the control has been 'invalidated' by the second window-that portion is the only part that needs to be redrawn.

Moreover, we'll see a more explicit form of 'invalidation'. We often need to write controls whose appearance depends on the state of the control (that is, the data values associated with the control). Sometimes, the control also allows users to perform actions that change the control's state. Whenever a user performs such an action, the result is that the display no longer reflects the actual state of the control-it must be updated. In this case, we can write code to identify the region that has been 'invalidated' by the user's action, and hence force a Paint event that repaints the invalidated region.

In this chapter, as we discuss these particular applications of regions, we'll introduce the notion of a clipping region, which is central to everything we've described above. We'll also describe the concept of invalidation, which is directly related to some of these tasks. We'll cover the following topics:

  • Specifying the size and shape of the clipping region, to describe which part of a drawing surface we want to modify

  • Detecting the clipping region specified on a Paint event, and hence minimize the amount of rendering work to the size of the client rectangle (or smaller!)

  • What happens when the form needs to be completely or partially redrawn, and the OS raises a Paint event

  • Using invalidation to force a Paint event from within the application

We'll start with a discussion of the clipping region-the central tool in this chapter-and how it is implemented in GDI+.


The Clipping Region

We can define the clipping region to be the region of the drawing surface in which the effects of drawing actions will actually be seen. But to really appreciate this definition, it's a good idea to consider the two different sets of circumstances in which the clipping region is used:

  • Sometimes, we would like to control the clipping region explicitly in our code, and hence use it to specify which region of the drawing surface we're drawing to

  • Other times, the framework needs to control the clipping region, at times when it needs to repaint portions of a form, or the entire form

Let's look at our definition in the light of these two different contexts, to understand what's really going on.

Cropping with the Clipping Region

In the GDI+ object model, the clipping region is a property of the Graphics object. This is because when we specify the clipping region in our code, it relates to the way that our drawing actions are represented on the drawing surface itself. We can use this to control the clipping region programmatically, and hence control precisely what region of the drawing surface is affected by a drawing operation.

It's useful to compare this to a piece of paper and a stencil. Think of the drawing surface as a piece of paper, and the clipping region as the stencil. Lay the paper on a table, and then lay the stencil over the paper and fix it. Once the stencil is in place, you can only draw onto certain regions of the paper-the regions that are exposed through the cut-outs of the stencil. Similarly in GDI+, when you apply a clipping region to a drawing surface, subsequent drawing operations only apply to the regions of the drawing surface exposed by the clipping region.

Let's look at a simple example that shows this stencil effect.

Using the SetClip Method

The GDI+ Graphics object provides us with a number of methods and properties that we can use to manipulate the clipping region. In the names of these methods and properties, they simply refer to the clipping region as the clip.

We'll use one of these methods-SetClip-to set the clipping region to be a rectangular region. For our convenience, we'll draw the rectangle first-just so that we can see it. Then we'll apply the rectangular clipping region, and then we'll perform some other drawing operations and see how the clipping region affects the results:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Define the rectangle that we'll use to define our clipping region
    Rectangle rect = new Rectangle(10, 10, 80, 50);

    // Draw the clipping region
    g.DrawRectangle(Pens.Black, rect);

    // Now apply the clipping region
    g.SetClip(rect);

    // Now draw something onto the drawing surface
    // (with the clipping region applied)
    g.FillEllipse(Brushes.Blue, 20, 20, 100, 100);
}

There are a number of important points to consider here, so let's step through it. At the time the Paint event is fired, the clipping region is automatically set to be the shape and size of the client rectangle (we'll return to this in a later section, when we discuss the ClipRectangle property).

Therefore, when we call the DrawRectangle method, it's like drawing onto a piece of paper without a stencil: the entire drawing surface is exposed and so the black rectangular outline is drawn to the drawing surface like this:

Click To expand

Next, we use the SetClip method to set the clipping region to be the region defined by our Rectangle object. This is like applying a rectangular stencil to the drawing area-subsequent drawing operations will affect only the region of the drawing surface that is exposed by the clipping region 'stencil'. Thus, when we draw the blue circular ellipse, the drawing surface gets to see only the part of the ellipse that is contained within the clipping region:

Click To expand

Thus, in this context, we're effectively using the clipping region to crop a sequence of drawing operations-so that they have no effect outside of the clipping region. This is often the only way to draw a partial graphic.

Unusual Clipping Regions

In the above example, the clipping region we chose was rectangular. In fact, when we're using the clipping region via the Graphics object like this, we can set it to be any Region object we like.

An Irregular Shaped Clipping Region

For example, let's create a very irregularly shaped region, use it to set the clipping region, and then perform a drawing operation:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create a Region object and make sure it's empty
    Region reg = new Region();
    reg.MakeEmpty();

    // Create an irregular Region...
    // ... add a circle to the region
    GraphicsPath gp = new GraphicsPath();
    gp.AddEllipse(10, 10, 50, 50);
    reg.Union(gp);
    // ... add a triangle to the region
    gp.Reset();
    gp.AddLine(40, 40, 70, 10);
    gp.AddLine(70, 10, 100, 40);
    gp.CloseFigure();
    reg.Union(gp);
    // ... add a rectangle to the region
    reg.Union(new Rectangle(40, 50, 60, 60));

    // Set the clipping region
    g.SetClip(reg, CombineMode.Replace);

    // Paint the client rectangle green.
    // Only the clipped region will be affected
    g.FillRectangle(Brushes.Green, this.ClientRectangle);

    gp.Dispose();
    reg.Dispose();
}

Here, we create a rather complex Region object by using a couple of Unions; and then we assign this strangely shaped region to the clipping region (using SetClip). Finally, we paint the entire client rectangle green. However, only the region exposed by the clipping region is painted green-once again, the clipping region acts as a stencil. The result is shown here.

Click To expand

In passing, it's worth explaining why we call the Region object's MakeEmpty method at the beginning of the example. We do this because the overloaded Region constructor we've used (with no arguments) generates a Region object that represents the entire infinite drawing surface. However, what we wanted to do here was start with an empty Region and add to it. Therefore, to create the empty Region, we use MakeEmpty.

I have created hundreds of custom controls of many varieties, and I have never had occasion to use a clipping region other than a rectangle. It's very rarely that we need to use complex GraphicsPath and Region objects for clipping-but we know it can be done.

Using Text for the Clipping Region

We can create a special effect by creating a GraphicsPath object, and using its AddString method so that it takes on the shape of the outline of the glyphs used in the string. Then we can use the GraphicsPath as the outline for the clipping region, and finally paint the clipped drawing surface with whatever brush or image we choose:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Prepare text string
    GraphicsPath gp = new GraphicsPath();
    gp.Addstring("Swirly", new FontFamily("Times New Roman"),
                (int)(FontStyle.Bold | FontStyle.Italic),
                144, new Point (5, 5), StringFormat.GenericTypographic);

    // Set clipping region
    g.SetClip(gp);

    // Paint the client rectangle using an image.
    // Only the clipped region will be affected
    g.DrawImage(new Bitmap("swirly.jpg"), this.ClientRectangle);

    gp.Dispose();
}

Here's the result. It looks more effective in color than here in grayscale, as you'll see if you try it out.

Click To expand

SetClip Method Overloads

In the three examples we've seen so far, we've used three different overloads of the SetClip method. In the first example we did this:

Rectangle rect = new Rectangle(10, 10, 80, 50);
g.SetClip(rect);

In this case, we pass a Rectangle object to the SetClip method-behind the scenes, the method creates a rectangular Region object from it and assigns it to the clipping region.

In another example, we created and built up a GraphicsPath object. Then we passed the GraphicsPath outline to the SetClip method, which (behind the scenes) used it to create a Region object, and set the clipping region to that:

GraphicsPath gp = new GraphicsPath();
...build the graphicspath...
g.SetClip(gp);

In fact, there are a total of nine overloads of the SetClip method. We can pass in a Rectangle object, RectangleF object, Graphics object, GraphicsPath object, or Region object.

Using the CombineMode Enumeration

We can also pass in a second parameter-a value from the CombineMode enumeration. If we pass this second parameter, it is used to express how the specified region is to be combined with the existing clipping region. There are six values in the CombineMode enumeration:

CombineMode.Replace
CombineMode.Complement
CombineMode.Exclude
CombineMode.Intersect
CombineMode.Union
CombineMode.Xor

If some of these look familiar, it's because they use the same names as (and have a similar effect to) the five 'set algebra' methods of the region object that we saw in the Chapter 6.

Let's quickly reconsider the example above in which we used the following overload of the SetClip method:

Region reg = new Region();
...build up the region...
g.SetClip(reg, CombineMode.Replace);

Here, the SetClip method replaces the existing clipping region with the region specified in the first parameter. If (for example) we'd wanted to use the intersection of the existing clipping region with the specified region, we would have used this:

g.SetClip(reg, CombineMode.Intersect);

Note that, when we pass a Region object to the SetClip method, we must also pass a value from the CombineMode enumeration. If we pass a Rectangle, RectangleF, Graphics, or GraphicsPath, then the CombineMode value is optional For more on the SetClip method's overloads, see the documentation at http://msdn.microsoft.com/library .

Other Clipping Region Methods and Properties

In addition to the SetClip method, there are four other methods within the Graphics class that we can use to configure the clipping region of a drawing surface:

Method | Description
ExcludeClip | Excludes a region from the existing clipping region.
IntersectClip | Intersects a region with the existing clipping region.
ResetClip | Sets the clipping region to be the infinite surface-effectively 'clearing' the clipping region. This is a useful method to use when we've finished with our custom clipping region, and we want to return to a situation where our drawing operations apply to the whole drawing surface.
TranslateClip | Offsets the clipping region by an X and Y delta. We can use this method if we want to use clipping in conjunction with translation transformations (which we'll explore in Chapter 8).

There are also a few properties of the Graphics class that facilitate clipping operations:

Property | Description
Clip | We can use this property to set or get the clipping region. We can only use the Region class to set clipping using this property, whereas we can use a variety of other structures and classes with the SetClip method. Unless we set this explicitly, it is set to be the infinite drawing surface.
ClipBounds | Gets a RectangleF that bounds the clipping region for the drawing surface.
IsClipEmpty | Returns true if the clipping region for the drawing surface is empty, and false otherwise.
VisibleClipBounds | Gets a RectangleF that bounds the visible clip region.
IsVisibleClipEmpty | Returns true if the intersection between the clipping region that we set and the clipping region of the window is empty, and false otherwise.

As you can see, the concept of visibility is considered sufficiently important that we can test for it. A clipping region is visible if its intersection with the visible area of the window is non-empty. We'll use the IsVisibleClipEmpty in particular, later in this chapter.

In the past few pages, we've taken a good look at some issues surrounding the notion that we can use the clipping region to create a cropping effect. Now let's look at the second context-where the clipping region is used in relation to painting and repainting the window.

Selective Repainting with the Clipping Region

In order to react to a Paint event, we have an event handler-and as you've seen, the event handler expects two arguments. For example:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    ...
}

The second of these arguments is a PaintEventArgs object, and contains information about the circumstances in which the Paint event was raised. In particular, we've used the PaintEventArgs object to get a reference to the Graphics object that represents the drawing surface that corresponds to the Paint event:

private void Form1_Paint(object sender,
                         System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.Red, this.ClientRectangle);
    ...
}

This is all stuff that we've seen in earlier chapters of this book. The point here is that the PaintEventArgs object passed into the Paint event handler also contains information about the clipping region that needs to be drawn by the event handler.

Thus, when our application receives a Paint event, it is not only told which drawing surface to paint to; it is also told what region of the drawing surface needs repainting. If it is possible to avoid repainting the entire window, then that's great-it means that there's less work for the application to do.

Let's consider an example.

Watching the ClipRectangle

The following Paint event handler does two things each time it runs. First, it locates the specified clipping region of the specified drawing surface (both given by the PaintEventArgs argument) and uses the DrawString method to paint in the specified text. Second, it writes a line to the console window that tells us the dimensions of the rectangular region it has just painted:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create the necessary objects to draw the text string
    String s = "When writing GDI+ code, there are three basic target ";
    s += <more text...>
    s += "be able to produce precise and effective graphics.";
    Font f = new Font("Times New Roman", 12);
    SizeF sf = g.MeasureString(s, f, 400);
    RectangleF rf = new RectangleF(10, 10, sf.Width, sf.Height);

    // Draw the text string
    g.DrawString(s, f, Brushes.Black, rf);
    f.Dispose();

    // Write info about the clipping rectangle to the console window
    Console.WriteLine("Clipping region painted: " + e.ClipRectangle);
}

Before we discuss this further, just try it out. When you first run the example, you should see something like this:

Click To expand

The screenshot itself is less interesting than the output to the console, which indicates the dimensions (in pixels) of the clipping region that our application needed to paint in order to produce it. You should see something like this:

Clipping region painted: <X=0, Y=0, Width=250, Height=200>

Whenever you make a change on your screen that requires this window to be repainted, it will fire a Paint event-and you'll see another line in the console window. For example, select another window and drag it so that it partially obscures the Form1 window, like this:

Click To expand

Now remove the second window so that the Form1 window is completely visible again. The operating system can't remember what the bottom-right corner looked like, so it must ask the application by firing its Paint event. Thus, you should see another line in the console window. Here's what I got when I moved the browser window away (we'll explain the exact dimensions in a moment):

Clipping region painted: <X=139, Y=26, Width=111, Height=174>

Now, try resizing the window by dragging the bottom-right corner with your mouse. What happens when you do this depends on whether you make the window bigger or smaller:

  • If you make it longer and/or wider, the operating system considers your request and works out that the window needs repainting, so it calls the Paint event of our application. We'll discuss this further in a moment.

  • Otherwise, the operating system considers your request and works out that it has enough information about the window to redraw it without calling the application's Paint event. In this case, it just uses the top-left portion of the existing client rectangle, and draws that into the client rectangle of the resized window.

One of the most interesting things about this example relates to how the dimensions of the clipping rectangle are calculated. Let's look at this next.

The Shape of the ClipRectangle

If you're running this example now, and you've experimented with it a little, then you've probably noticed a couple of things. First, the clipping region passed into the Paint event is always a rectangle. But in Chapter 6, we said that a Region object can represent almost any shape we like! Is there a contradiction here?

In fact, there is no contradiction. In general, a Region object doesn't have to be rectangular-it can take on any of the shapes we described in Chapter 6. If we use a clipping region for cropping (as we discussed earlier in this chapter), we can use the SetClip method to set the clipping region to be any Region object we like.

However, when it comes to painting and repainting windows, we work in rectangles-generally because the operating system likes to work in rectangles, and because they are easy to work with in a raster-based environment. Thus, when the Paint event handler is called, the clipping region is always set to be the rectangle that the operating system says it needs to repaint.

Specifying the Dimensions of the ClipRectangle

So, when the Paint event is fired, the clipping region will always be a rectangle. The next question is: How are the dimensions of the clipping rectangle decided?

In fact, the dimensions of the clipping rectangle are decided by the operating system that needs to render the visible part of the graphic to the drawing surface. In general, the operating system asks for the smallest possible rectangle that will contain all the information it needs to perform the rendering action. There are two reasons for this: it minimizes the amount of data that needs to be passed between the application and the operation system, and it also minimizes the amount of rendering work that the OS must do to re-render the window. Thus:

  • When we first run the application, we need to render the entire window. In this case, the clipping rectangle is equal in size to the client rectangle. So, suppose your application defines the client rectangle to be 250×200 pixels:

    private void InitializeComponent()
{
    ...
    this.ClientSize = new System.Drawing.Size(250, 200);
    ...
}

    Then the output to the console window should confirm that the clipping rectangle is a 250×200 pixel rectangle, whose top-left corner is at (0, 0):

    Clipping region painted: <X=0, Y=0, Width=250, Height=200>

  • If you make the window shorter and wider, then the operating system forgets about the bottom-left section of the graphic that it no longer needs. But it must fire a Paint event to request information about the extra rectangular region in the top-right corner:

    Click To expand

    In the example above, we've changed the client window size from 250×200 to 400×150. Therefore, the console window should confirm that the clipping rectangle is the new upper-right region:

    Clipping region painted: <X=250, Y=0, Width=150, Height=150>

  • If, instead, you make the window longer and narrower, then the operating system forgets about the upper-right section of the graphic that it no longer needs, but fires a Paint event to request information about the extra rectangular region in the bottom-left corner. In this example, we've changed the client window size from 250×200 to 150×250, so the console window should show that the clipping rectangle is the new lower-left region:

    Clipping region painted: <X=0, Y=200, Width=150, Height=50>

    Click To expand

  • If you make the window longer and wider, then it's a little different. Although the actual new area is L-shaped, the Paint event only allows the OS to request information about a rectangular clipping region. The smallest possible rectangle that can cover the missing part of the graphic is the entire client rectangle.

    In the example below, we've changed the client window size from 250×200 to 350×300. The clipping rectangle is the same as the client rectangle:

    Clipping region painted: <X=0, Y=0, Width=350, Height=300>

    Click To expand

  • If you hide part of the window and then reveal it again, the OS requests the smallest rectangle required to replace the part of the graphic that was obscured. So in the example we saw earlier, we needed a 111×174 clipping rectangle placed at (139, 26) to replace the missing part of the graphic:

    Clipping region painted: <X=139, Y=26, Width=111, Height=174>

    Click To expand

Using the ClipRectangle to Minimize Drawing Work

What we learn from the discussion above is that, when our form is being painted (or re-painted) to a screen, the operating system plays an important part. It takes a lot of responsibility for deciding how much information is needed from the application in order to render the window on the screen-and if it needs any information, it fires a Paint event.

It's also interesting to look at this from within the Paint event handler. Whenever the Paint event handler is called, it will execute completely in order to generate the graphic, and then the portion required by the operation system (the portion specified by the PaintEventArgs.ClipRectangle property) is passed back to the operating system so the window can be (re)painted.

Depending on the nature of the graphic, we can sometimes use the information in the ClipRectangle property to minimize the amount of work the Paint event handler has to do. For example, suppose we have an application that creates a two-dimensional grid effect. There are lots of ways we could achieve this effect-but we can write the Paint event handler in such a way that a re-paint requires minimal work within the event handler.

Let's look at some code to illustrate this. We're going to write some code that creates the following graphic:

Click To expand

We'll also write some logging information to the console window, so we can track how much work the event handler is doing.

We'll build the grid effect by using a nested loop to write the rows and columns of rectangles, and the text strings within them. The nested loop is shaded in the code that follows:

private void Form1_Paint (object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;

    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Create essential objects for painting text strings
    SizeF sizeF = g.MeasureString("Test", this.Font);
    StringFormat sf = new StringFormat();
    sf.LineAlignment = StringAlignment.Center;

    // Set properties of the grid
    int cellHeight = (int)sizeF.Height + 4;
    int cellWidth = 80;
    int nbrColumns = 50;
    int nbrRows = 50;

    // Output general info to console
    Console.Wri---------------");
    Console.WriteLine("e.ClipRectangle = " + e.ClipRectangle);
    Console.WriteLine("The following cells need to be redrawn " +
                      "(in whole or in part):");

    // Draw the cells and the output-to-console
    for (int row = 0; row < nbrRows; ++row)
    {
        for (int col = 0; col < nbrColumns; ++col)
        {
            Point cellLocation = new Point(col * cellWidth,
                                               row * cellHeight);
            Rectangle cellRect = new Rectangle(cellLocation.X,
                                               cellLocation.Y,
                                               cellWidth, cellHeight);
            if (cellRect.IntersectsWith(e.ClipRectangle))
            {
                Console.WriteLine("Row: {0} Col:{1}", row, col);
                g.FillRectangle(Brushes.LightGray, cellRect);
                g.DrawRectangle(Pens.Black, cellRect);
                String s = String.Format("{0},{1}", col, row);
                g.DrawString(s, this.Font, Brushes.Black, cellRect, sf);
            }
            else
            {
                //do nothing...
            }
        }
    }
}

Here, the outside loop builds each row one at a time. For each row, the inside loop builds a number of rectangles side by side. But the important thing here is the if…else clause within the inside loop:

if (cellRect.IntersectsWith(e.ClipRectangle))
{
    ...
}
else
{
    //do nothing...
}

This clause states that the event handler will only build this rectangle if its intersection with the ClipRectangle is not empty. In other words, it will not build the rectangle if the operating system isn't going to render at least part of that rectangle to the drawing surface.

To see the effect of this, we simply need to run the example. Without the if…else clause, the event handler would need to generate 50 rows and 50 columns-that's 2500 rectangles. But since the if…else clause compares the intended position of each rectangle with the ClipRectangle, the only rectangles generated by our code are the ones that will actually be painted to the visible drawing surface. The output in the console window shows exactly that:

e.ClipRectangle = {X=0,Y=0,Width=350,Height=200}
The following cells need to be redrawn (in whole or in part):
Row:0 Col:0
Row:0 Col:1
Row:0 Col:2
Row:0 Col:3
Row:0 Col:4
Row:1 Col:0
Row:1 Col:1
Row:1 Col:2
Row:1 Col:3
Row:1 Col:4
     <snipped...>
Row:11 Col:0
Row:11 Col:1
Row:11 Col:2
Row:11 Col:3
Row:11 Col:4

With the ClipRectangle at 350×200 pixels, the event handler only needs to draw the first 11 rows and 4 columns. The (non-empty) intersection of these rectangles with the ClipRectangle is the graphic that is returned to the operating system for rendering. The same applies if you resize the window-as you'll see if you try it out.

Using the IsVisibleClipEmpty Property to Minimize Drawing Work-A Comparison

In fact, we could achieve a similar effect using the Graphics.IsVisibleClipEmpty test instead of the Rectangle.IntersectsWith(e.ClipRectangle) test. To do this, we only need to change the above code as shown:

Rectangle cellRect = new Rectangle(cellLocation.X,
                                   cellLocation.Y,
                                   cellWidth, cellHeight);
g.SetClip(cellRect);
if (! g.IsVisibleClipEmpty)
{
    Console.WriteLine("Row:{0} Col:{1}", row, col);
    g.FillRectangle(Brushes.LightGray, cellRect);
    g.DrawRectangle(Pens.Black, cellRect);
    String s = String.Format("{0},{1}", col, row);
    g.DrawString(s, this.Font, Brushes.Black, cellRect, sf);
}
else
{
    // do nothing...
}

The difference is that:

  • When we used IntersectsWith, we compared each rectangle with the clipping rectangle.

  • When we use IsVisibleClipEmpty, we compare each rectangle with the client rectangle.

Thus, when we first run the application, the clipping rectangle is the same as the client rectangle, and the resulting output is identical. But if we try to resize the window, then the clipping rectangle could be smaller than the client rectangle (as we saw in earlier sections of this chapter), and the amount of drawing work to be done is correspondingly different.

So, if we're expecting a lot of window resizing, the IntersectsWith test is a better choice-because it allows the event handler to get away with doing even less work! In other circumstances, the IsVisibleClipEmpty test would be a better choice. The only way to choose between them is to have an awareness of how your application is likely to be used, and to perform performance testing using the different techniques.

Forcing a Repaint of the Entire Window

Sometimes, the content of our window is such that, if the size of the window changes, we want to repaint the entire window. For example, suppose our window contains a bitmap that we have stretched to fill the client rectangle (like the example in Chapter 5):

Click To expand

In this case, it's not sufficient simply for the Paint event to supply a region containing a portion of the graphic, because any resize of this window will affect the entire client rectangle.

In this case, we need to be able to force a resize to cause the entire client rectangle to be repainted. We can't overwrite the PaintEventArgs.ClipRectangle property, because it's a read-only property.

However, we can force a complete redraw by setting the ResizeRedraw property of the Form to true. Add a Load event to the form, and set the ResizeRedraw property there:

private void Form1_Load(object sender, System.EventArgs e)
{
    this.ResizeRedraw = true;
}

This causes the ClipRectangle to be the same as the client rectangle every time the window is redrawn:

Clipping region painted: {X=0,Y=0,Width=250,Height=200}
Clipping region painted: {X=0,Y=0,Width=311,Height=328}
Clipping region painted: {X=0,Y=0,Width=433,Height=113}
Clipping region painted: {X=0,Y=0,Width=313,Height=303}
...

Clipping at the Pixel Level

As we've seen, one of the main purposes of the clipping algorithms implemented by GDI+ (and, indeed, by other drawing packages) is to support the behavior of overlapping windows on the computer screen. This fact is one that strongly influences the way that clipping behaves at the pixel level.

The reason for this is that on our raster-based computer screen, each window is composed of an array of whole pixels. Each edge of each window coincides with the edge of a row or column of pixels, and each edge of the client rectangle also coincides with the edge of a row or column of pixels. There is no anti-aliasing (of the sort we described in Chapter 2) and the window's edge, or at the client rectangle's edge.

What this means is that, at the pixel level, our clipping region is also composed of whole pixels. It is not composed of half-pixels, in any sense. (In other words, the edge of a clipping region does not include part-pixels; neither does it include pixels whose color is influenced by both the color of the region and the color of the neighboring thing outside the region.)

To illustrate this point, we can compare the close-up results of a couple of examples. In both examples we will apply anti-aliasing. In the first example, we'll take a pixel-level view of a clipping region drawn onto a drawing surface:

Graphics g = e.Graphics;
g.FillRectangle(Brushes.White, this.ClientRectangle);

g.SmoothingMode = SmoothingMode.AntiAlias;
Rectangle rect = new Rectangle(2, 2, 4, 4);
g.SetClip(rect);
g.FillRectangle(Brushes.Black, this.ClientRectangle);
Click To expand

In the second, we'll look at a rectangle drawn onto the drawing surface with anti-aliasing turned on:

Graphics g = e.Graphics;
g.FillRectangle(Brushes.White, this.ClientRectangle);

g.SmoothingMode = SmoothingMode.AntiAlias;
Rectangle rect = new Rectangle(2, 2, 4, 4);
g.FillRectangle(Brushes.Black, rect);
Click To expand

The effect of anti-aliasing in the second drawing is clear, and similar to the effects we saw in Chapter 2). However, in the first example the anti-aliasing setting has been ignored and the region consists of whole pixels only.


Invalidation

Broadly speaking, there are two groups of circumstances that can cause our drawing surface to need to be redrawn-external effects and internal effects. We have discussed some external effects already. For example, if a user resizes our window, then this is an action that is outside the control of the application: the user can change the window size whenever they want. When it does happen, the operating system will detect the user's action, and raise a Paint event that is then handled by the application.

In the other group, it is possible that things may happen within the application that cause us to want to redraw the graphic. For example, if the user performs an operation on the control that changes the state of the control-for example, by checking a checkbox within the control-then we will want to be able to redraw the checkbox to reflect the user's action.

In circumstances like this, we need to be able to force a Paint event from within the application. Invalidation is a mechanism that allows us programmatically to force the framework to raise a Paint event. In effect, we use invalidation to tell GDI+ that a specified portion of the window or custom control is no longer valid, and needs to be repainted.

Using State in our Control

Most custom controls involve a certain amount of state-that is, internal data that is related to the control. For example, a control with a checkbox is likely to need to be able to keep a record of the current value of the checkbox-it would do so by storing the value in an internal member variable within the control. When the user checks or unchecks the checkbox, the control needs to update the internal variable (that is, update the state) to reflect this change.

There are various ways that we can maintain the state of a control. We might keep it in internal member variables, or in a specially designed data structure, or even in a special state class object.

However we store our control's state internally, the fact remains that the state probably has a significant effect on the appearance of the control. So, if a user performs an action that changes the state of the control, we will probably need to take account of these changes and redraw the control accordingly. As we've said, this means that we must be able to force a Paint event from within the application-and this is the purpose of invalidation.

The best way to react to an action like this is as follows:

  1. Change the state. In the case of the checkbox, we might have a Boolean member variable that is set to true or false, and needs to be changed when the checkbox is clicked.

  2. Identify the region of the drawing surface that is affected by the change of state. For example, if the user clicked on a checkbox, then we will need to change the appearance of the checkbox. Depending on the exact functionality of the control, we may need to update text or other regions of the drawing surface too.

  3. If the region identified in Step 2 is currently visible, then invalidate it. To do this, we use the Form.Invalidate method-the invalidated region is passed as an argument to this method.

  4. Wait for the Paint event to come through to our custom control. The clipping region will be set appropriately. Alternatively, force a Paint event by using the Update method.

This approach has several benefits to recommend it. First, it consolidates almost all painting code for the check box into a single method-the Paint event handler. As a consequence, our code is cleaner, more modular, and therefore more maintainable. This is arguably the most important benefit.

I say 'almost all', because there is one exception to this rule-namely, implementing an operation that involves dragging something with the mouse. In this case, the dragging effect is shown semi-transparently on the screen. The nature of a drag event is different from most other events, because it is not a discrete event; if we go through the process of changing state, invalidating, and then waiting for the Paint event with every mouse movement, the dragging operation is likely to appear jerky and amateurish.

The solution to this problem is to include the drawing code within the mouse movement event. By using bit block transfer (BitBlt), we can create a very sophisticated appearance to our drag and drop operations, such as that of the file manager under Windows XP. We will cover this technique in detail in Mouse Events and Cursors.

Second, in a complex custom control we may have many event handlers that all need to be able to change the control's state in response to user actions and operating system actions-including mouse event handlers, keyboard event handlers, timer event handlers, and others. This approach allows us to standardize the way that all these event handlers change the appearance of the custom control. They all use the same approach: change the state and invalidate.

Third, this is the standard approach that developers follow when building custom controls. By following this pattern, we will ensure that our custom controls are easier for maintenance developers to understand. The structure of our control will not be a surprise to them.

How Invalidation Works

We invalidate a window by calling the Invalidate method, which is implemented by the Control class and is thus available through the Form class (because the Form class derives from the Control class).

Of course, invalidation only applies to drawing surfaces that are rendered to the screen. The other two main types of drawing surfaces, printers and images, don't have any use for an invalidation mechanism. The print preview drawing surface, although rendered to the screen, really has the behavior of a printer, and also doesn't use an invalidation mechanism.

In detail, the way that the mechanism works is this:

  1. Somewhere in our application, our code makes a decision that the contents of our window or custom control need to change. We then call the Invalidate method, which is overloaded. If we call the method with no arguments, the entire control or window is invalidated. Alternatively, we can pass a rectangle or a region to the Invalidate method, which will then invalidate just the area that we specify.

  2. At some time after we call the Invalidate method, GDI+ will send a Paint event to our window or custom control. We have no guarantee as to when this Paint event will be raised-see Point 4 below.

  3. When the Paint event is raised, GDI+ sets the clipping region to the area or region that we invalidated. If we invalidate several rectangles or regions at once, GDI+ may coalesce these into one region and raise a single Paint event.

  4. The Paint event may not come through immediately. If, after invalidating, we continue to do our own processing, the Paint event will not come through until we relinquish control of the form.

    In addition, if we have invalidated several windows and controls, we can't dictate in what order our Paint events are raised.

  5. If we want to force the raising of the Paint event for our invalidation, we can do so by calling the Form.Update method, which is also inherited from the Control class. If we call the Update method, a Paint event will occur before the Update method returns to the method that called it.

  6. In many cases, the clipping region that comes with the Paint event is the same rectangle or region as that passed to the Invalidate method, unless GDI+ coalesces multiple invalidations into one.

Note that invalidation uses the same coordinate system behavior as clipping-and hence invalidation occurs at the granularity of pixels, not sub-pixels. This is not really surprising, since invalidation is intricately dependent upon clipping capabilities.

An Example

In this example application, the 'state' will consist of the value of a single variable, strColor. To enable this value to be accessible to various event handlers, we declare it as a private member variable. This means that the variable has application scope:

public class Form1 : System.Windows.Forms.Form
{
    private string strColor;

The application's state will need to be initialized. To do this, we assign the value red to the string strColor within the form's InitializeComponent routine:

private void InitializeComponent()
{
    ...
    strColor = "red";
}

Now, our application will need two event handlers. First, we'll need a Paint event handler to paint to the drawing surface. We'll also write a Click event handler.

The Paint Event Handler

Let's write the Paint event handler first. We'll use it to draw a traffic-control type graphic, which displays one of three colored 'lights'-red, yellow, or green-depending on the value of strColor. When we first fire up the application, the red light will show.

The code in the Paint event handler itself is nothing unusual. We use a C# switch statement to detect the value of strColor and hence paint the correct 'light'. We also write the dimensions of the ClipRectangle to the console window, because that will be of interest to us in a moment:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // Report dimensions of clipping rectangle to console
    Console.WriteLine("e.ClipRectangle = " + e.ClipRectangle);

    // Draw outline
    g.FillRectangle(Brushes.Black, 10, 10, 50, 150);
    g.DrawEllipse(Pens.White, 15, 15, 40, 40);
    g.DrawEllipse(Pens.White, 15, 60, 40, 40);
    g.DrawEllipse(Pens.White, 15, 105, 40, 40);

    // Use application state (strColor) to draw
    // exactly one of the three lights
    switch(strColor)
    {
        case "red":
            g.FillEllipse(Brushes.Red, 15, 15, 40, 40);
            break;
        case "yellow":
            g.FillEllipse(Brushes.Yellow, 15, 60, 40, 40);
            break;
        case "green":
            g.FillEllipse(Brushes.Green, 15, 105, 40, 40);
            break;
        default:
            g.FillEllipse(Brushes.Red, 15, 15, 40, 40);
            break;
    }
}

Here's what this looks like when we start the application. The light at the top is colored red.

The Click Event Handler

Now, the Click event will force a change to the state of the application. The Click event handler will check the current value of strColor and change it to something different:

private void Form1_Click(object sender, System.EventArgs e)
{
    // Change application state by altering value of strColor
    switch(strColor)
    {
        case "red":
            strColor="yellow";
            break;
        case "yellow":
            strColor="green";
            break;
        default:
            strColor="red";
            break;
    }

Of course, once we've changed the state (by changing the value of strColor), then the display shown on the drawing surface no longer corresponds to the application's state. In this case, for example, when the Click event causes the value of strColor to change from red to yellow, then the drawing surface itself needs to be redrawn to reflect the change-we need to show the yellow light instead of the red one.

To do this, we invalidate the region of the drawing surface that needs redrawing. A Paint event will be fired sometime after that-but to force the Paint event, we follow the Invalidate method call with a call to the Update method:

        // Invaliate the region containing the traffic lights
        this.Invalidate(new Rectangle(10, 10, 50, 150));

        // Request an update
        this.Update();
    }

Each time we click the window, the state changes; the rectangular region is marked as invalid; and then the Paint event fires and the window is repainted so that it reflects the state of the control:

Click To expand
Watching the Clipping Retangle

The final thing to note here is that the first time the window is drawn, the clipping rectangle is the same size as the client rectangle. But whenever we change the state, we only invalidate the small rectangle that contains the traffic light graphic:

this.Invalidate(new Rectangle(10, 10, 50, 150));

Therefore, when the Paint event runs, the clipping rectangle is set to be the invalidated region-and is the only portion of the window that is redrawn. We can see this in the output to the console window:

e.ClipRectangle = {X=0,Y=0,Width=120,Height=180}
e.ClipRectangle = {X=10,Y=10,Width=50,Height=150}
e.ClipRectangle = {X=10,Y=10,Width=50,Height=150}
e.ClipRectangle = {X=10,Y=10,Width=50,Height=150}


A Few Clipping Tips

In my experience, if the application being developed needs a more elaborate clipping scheme, using the following technique leads to a more maintainable application:

  • Store the clipping region as part of the state of the window-for example, in a private member variable, rather than in the Paint event. This allows any event handler or helper function to access and change the clipping region if necessary.

    It also helps future maintenance programmers to identify the clipping algorithm. This technique means that we would tend to use only the Replace member of the CombineMode enumeration.

  • When you need to modify the clipping region, modify the state variable; and then explicitly replace the clipping region before you draw.

  • When you create a complicated control with drawing operations spread over many methods, have each method set its clipping region before it starts to draw. (In other words, don't allow the method to make assumptions about the value of the clipping region stored in state.) Setting the clipping region is a cheap operation; it takes very little processing time. Even if we unnecessarily set the same clipping region many times, the resulting clarity of code more than makes up for the slight increase in processing time.


Summary

In Chapter 6, we developed the idea of a region. In this chapter, we focused on the clipping region, which is the region of the drawing surface in which the effects of drawing actions will actually be seen. We looked at two important applications of the clipping region:

  • First, we looked at how we can apply the clipping region to our graphical drawing operations like a stencil, and hence create a cropping effect. In this application, we control the clipping region programmatically, using methods and properties of the Graphics object (such as the Clip property and the SetClip method).

  • Second, we saw that when a Paint event is raised, the event handler doesn't always have to regenerate the entire drawing surface. The event handler is passed information about how much of the drawing surface it needs to redraw, in the form of the PaintEventArgs.ClipRectangle. Within the event handler, we can ignore this information if we choose; but sometimes, it's possible to use the ClipRectangle information to minimize the amount of work that the event handler needs to do-and hence improve the efficiency of the event handler.

The ClipRectangle is an important tool for us. We studied the dimensions of the ClipRectangle requested in a number of different circumstances, particularly in situations where our control is being rendered to a window on a screen:

  • Sometimes, the ClipRectangle is the same as the client rectangle. For example, this happens when the application first draws the drawing surface-it must generate enough graphic to fill the client rectangle.

  • If the user resizes the window, then the operating system will compare the old window size and the new window size-if the new size is longer and/or wider than the old size then the resulting client rectangle will expose regions of the drawing surface that weren't previously exposed. Since the operating system doesn't know what to render in those regions, it fires a Paint event on our application-it passes a ClipRectangle that tells the event handler what region of the drawing surface needs to be regenerated. The ClipRectangle will be the smallest possible rectangle that contains the missing information.

  • If the user obscures part of our window with another window, and then reveals it again, then the operating system needs to redraw the section that was obscured. Again, it fires a Paint event, passing a ClipRectangle that corresponds to the obscured portion of the drawing surface (which is the only part that needs to be repainted).

  • If the state of our application changes (for example, as a result of a user's mouse or keyboard event), and this needs to be reflected in the appearance of the control, then we need a way to have the control repainted. We can force a Paint event programmatically from within the application by using invalidation. We can invalidate the entire drawing surface, or just the region that is affected by the change of state-the dimensions of the invalidated region are passed back into the Paint event via the ClipRectangle, and so only the invalidated region is repainted.

We took a brief look at how to use state in our control. In general, it's not a good idea to maintain a lot of state within the Paint event handler itself, because we want the event handler to be as efficient as possible. We also want to be able to access state from other event handlers. Therefore it's better to hold state in member variables or something similar, with application scope. The result is more modularized application code in which the drawing code is isolated in the Paint event handler (with one exception-the mousemove event), and hence the code is easier to maintain.

We can set the clipping region using a Rectangle, RectangleF, Graphics, GraphicsPath, or Region object. We can also compose the clipping region using standard set algebra operations-Complement, Exclude, Intersect, Union, or Xor.

In the next chapter, we're going to move away from the subject of regions and tackle another important tool-transformations.


Chapter 8: Transformations

Overview

In this chapter we're going to look at the way in which GDI+ implements an advanced and important graphics tool-the transformation. Although not ubiquitous in custom control development, transformations can be very useful in certain circumstances. In fact, later in the book, we'll see how to use transformations in combination with other techniques to build a scrolling control that has more capabilities than the stock .NET Windows Forms scrolling controls.

In this chapter we will:

  • Investigate coordinate systems and conversions, and illustrate how to implement scaling, translation, rotation, and shearing transformations using the methods available in GDI+

  • Understand why transformations are not applied to invalidation or clipping operations

  • Look at some examples in which custom control developers may use transformations

First, we'll take a little time to identify the different types of transformation and the practical results they give.


Transformations in GDI+

In GDI+, when we apply a transformation to a drawing surface, it applies to all subsequent drawing operations. As each drawing operation executes, GDI+ alters the results of the drawing operation by applying the 'current' transformation to it.

Let's look at a few 'experiments' to demonstrate this. First, as our 'control', is a window into which we have drawn the letter ‘A', untransformed:

Click To expand

A scaling transformation changes the size of the result. So if we apply a scaling transformation that increases the size of the subsequent drawing operations, the result when we draw our letter ‘A' appears to be larger:

Click To expand

A translation transformation has the effect of shifting the results of subsequent operations a specified distance left or right, and up or down. So, if we apply a translation to our drawing surface and then draw our letter ‘A', we might get something like this:

Click To expand

A rotation transformation has the effect of rotating elements through a specified angle about a specified point. The following window shows an example in which we've applied a rotation to the drawing surface and then drawn our letter ‘A' (although, as we'll see later, the example shown isn't quite as straightforward as it looks):

Click To expand

The shearing transformation reminds me of a cartoon image in which the cartoonist wants to convey an image like a car that is moving fast, so they lean the car backwards. Mathematically, a shear is a transformation that turns a rectangle into a parallelogram. Here's the effect is has on our letter ‘A':

Click To expand

These are the four basic types of transformation that we'll we working with. In a moment, we'll dig into the specifics of each operation, but first, we need to return to the subject of coordinates, to understand a little more about the different coordinate systems we are working with.

Coordinate Systems and Transformations

We discussed coordinate systems in some detail back in Chapter 2, when we first looked at drawing surfaces, and we will do again when we propose an alternative coordinate system for developing custom controls. The influence of coordinate systems has been seen in our treatment of texts, fonts, and images, and will also loom large when we need to cover printing. In fact, in GDI+ there are three different coordinate systems that we must know about:

  • World coordinates

  • Page coordinates

  • Device coordinates

The world coordinate system is the coordinate system that we reference in our application. Whenever we pass coordinates to any drawing function, we express those coordinates in the world coordinate system. The configuration of the page coordinate system determines the units in which we draw and its origin is at the upper left corner of the client area. The device coordinate system is based on pixel coordinates-again relative to the upper left corner of the client area (or the upper left corner of the printable area of the page).

So when GDI+ outputs to a device (like a screen) there are two coordinate conversions that take place-a world transformation (which converts from world coordinates to page coordinates), and a page transformation (which converts from page coordinates to device coordinates).

In fact, this chapter focuses specifically on the world transformation. The world transformation is the mechanism by which we can carry out translations, scaling operations, rotations, and shearing. (The page transformation is specified using the PageUnit and PageScale properties of the Graphics class, the first of which we've already encountered in our discussion of texts and fonts, and will be discussed again in more detail within .)

In the world coordinate system, coordinates are often expressed with floating-point precision. By default, the page coordinate system unit of measure is Pixel. If we set the page coordinate system to another value (such as Inch, Millimeter, Document, etc.) our coordinates are then expressed in the associated unit of measure (inch, mm, 1/300 inch respectively). As we noted back in Chapter 2, the most useful unit for making custom controls is Pixel. The device coordinate system is always pixel-based-this is related to the fact that all drawing surfaces in GDI+ are raster-based.

World Transformations

When a world transformation is set it applies to all items that are subsequently drawn on the surface. For a large part of this chapter, we'll make use of certain convenience methods made available by the Graphics class, which help us carry out the most common transformations. It is worth bearing in mind though that, under the hood, what we are really doing is manipulating a Matrix object, which is set up by the methods.

A Matrix object defines what is called an affine transformation (a transformation that preserves collinearity-meaning that parallel lines always stay parallel after the transformation). In GDI+, while we can carry out simple transformations, or complex ones, they will all be affine transformations-we don't have any means for doing other transformations.

The convenience methods we're interested in are:

Method | Description
ScaleTransform | Applies a scaling to the world transformation.
TranslateTransform | Applies a translation (horizontal and/or vertical shift) to the world transformation.
RotateTransform | Applies a rotation about the point (0, 0), specified in degrees, to the world transformation.
MultiplyTransform | Appends a transformation Matrix object to the world transformation. This means that if we had an existing transformation in place, using the MultiplyTransform method appends the specified transformation to the existing transformations.
ResetTransform | Resets the transformation to the identity matrix. This means that no transformations are in effect. This basically clears all existing world transformations.

We'll see examples of the use of each of these methods over the course of this chapter and we'll return to the topic of matrices before we show an example of shearing (where coincidentally we use the MultiplyTransform method). Let's start with one of the simplest-the scaling transformation.

Scaling Transformations

A scaling transformation simply allows us to apply a horizontal scaling factor and a vertical scaling factor. For example, if the scaling factor applied is 1.0, then no scaling takes place. If the scaling factor is 0.5 then subsequent elements are scaled to half-size, and if the scaling factor is 2.0 then the elements are drawn at double-size.

Here's a simple Paint event handler that contains code to illustrate this:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    // First, draw a non-scaled rectangle and circle
    g.DrawRectangle(Pens.Black, 10, 10, 50, 50);
    g.DrawEllipse(Pens.Black, 10, 10, 10, 10);

    // Now apply the scaling transformation
    // This will scale subsequent operations by 2x in horizontally
    // and 3x vertically
    g.ScaleTransform(2.0f, 3.0f);

    // Now draw the same rectangle and circle,
    // but with the scaling transformation
    g.DrawRectangle(Pens.Black, 10, 10, 50, 50);
    g.DrawEllipse(Pens.Black, 10, 10, 10, 10);
}

When run, the results look like this:

Click To expand

Here, there are four drawing operations, separated by one transformation method call. The first two drawing operations draw a square and a circle to the drawing surface. At the time these two drawing operations are called, there is no transformation specified, so in fact, GDI+ adopts the 'default' world transformation-namely, it maps the world coordinate system directly onto the page coordinate system, mapping (0, 0) in the world coordinate system to (0, 0) on the page coordinate system and applying no scaling, translation, or shear transform effects. The result is the 'natural' square and circle you see above.

Then, we apply the ScaleTransform method and draw the same objects again. This time, the transformation has the effect of scaling the dimensions of the rectangle and circle (doubling in the X direction and tripling in the Y direction). Note that it has also scaled the width of the pen used to draw the shapes. The ScaleTransform method can also be used to yield reflections. We'll see this when we return to the topic of matrices.

Translation Transformations

In the development of custom controls, the translation transformation is probably the most common transformation. With it, we apply a shift in the X and/or Y directions. Positive values shift the resulting graphics operations down and to the right. For example, if we were drawing a line starting at point (10, 10), and we applied a translation transformation of X=5, Y=10, then the line would actually be drawn at pixel (15, 20).

Consider the following code:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Font f = new Font("Times New Roman", 24);

    //Draw text to the surface
    g.DrawString("Translation", f, Brushes.Black, 0, 0);

    //Translate the text 150 pixels horizontally and 75 vertically
    g.TranslateTransform(150, 75);

    //Draw the translated text
    g.DrawString("Translation", f, Brushes.Black, 0, 0);
}

This results in a window with the word Translation written on it twice in different places, as shown in the following diagram:

Click To expand

Mathematically, one can consider the translation to have moved the origin to which the drawing operation is related. In effect, applying a translation transform means that the origin (0, 0) of the drawing surface has been moved from the top left corner of the client rectangle, by a distance delta X=150 pixels to the right, and delta Y=75 pixels down. The drawing operation is oriented along axes that are parallel to the original axes (because we have only applied a translation, and not a rotation or shear).

Remember that GDI+ applies the current world transformation to each drawing operation in turn, converting world coordinates to page coordinates. Thus, for the first drawing operation here (where there is no world transformation explicitly defined), GDI+ adopts the default behavior-mapping the world coordinate system directly onto the page coordinate system, and hence placing the word Translation faithfully near the upper left corner of the client area.

Then, the following call applies a world transformation of a translation (by 150 pixels in the horizontal and 75 pixels in the vertical direction):

g.TranslateTransform(150, 75);

Thus the page coordinates for the second drawing operation are modified (from their origin of the upper left corner of the client area) by this amount and thus the second DrawString call yields a piece of text away from the corner.

In other words we can say:

X(Page Coordinate) = X(World Coordinate) + Delta X
Y(Page Coordinate) = Y(World Coordinate) + Delta Y

The primary reason for using this transformation is when we want to create a virtual space. By applying a translation transformation in the appropriate way, our drawing code does not need to be aware that it is drawing into a virtual space.

Cumulative Transformations and Non-cumulative Transformations

If we apply multiple transformations, they have a cumulative effect. Replace the code in the Paint event, as follows:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);

    for (int i = 1; i <= 5; ++i)
    {
        // first, draw a rectangle with the current translation
        g.DrawRectangle(Pens.Black, 10, 10, 30, 50);

        // add a translation to the existing transformation
        g.TranslateTransform(2, 10);
    }
}

When run, this generates a series of identically sized rectangles in different positions like so:

Click To expand

Of course, combining positive and negative values can be used for a variety of effects.

If we want to apply transformations in such a way that they are not cumulative, then we can call the ResetTransform method:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, ClientRectangle);

    // first, draw a rectangle with no translation
    g.DrawEllipse(Pens.Black, 20, 20, 30, 50);

    // translate by −15 pixels in horizontal direction
    g.TranslateTransform(−15, 0);
    g.DrawEllipse(Pens.Black, 20, 20, 30, 50);

    // reset the transformation
    // translate by 30 pixels in vertical direction
    g.ResetTransform();
    g.TranslateTransform(0, 30);
    g.DrawEllipse(Pens.Black, 20, 20, 30, 50);
}

This has the results that we would expect:

Click To expand

The first ellipse is drawn faithfully at (20, 20), while the second is translated a distance 15 pixels left. Then we reset the world transformation to the default, before applying a downward translation. Thus, the two transformations in this example are not cumulative.

Rotation Transformations

With this transformation, we apply a rotation in the clockwise direction, specified in degrees. The center of rotation is the current origin of the coordinate system.

To examine this, run the following code:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Font f = new Font("Times New Roman", 24);

    //Draw text to the surface
    g.DrawString("Rotation", f, Brushes.Black, 0,0);

    //Rotate the text through 45 degrees
    g.RotateTransform(45);

    //Draw the rotated text
    g.DrawString("Rotation", f, Brushes.Black, 0,0);
}

Again it's instructive to consider an annotated screenshot:

Click To expand

Here, the effect of the transformation is to rotate the drawing surface (axes and all) against which the drawing operation is referenced.

Since rotation transformations rotate around the current origin (which, by default, is the upper left corner), in this example the rotated DrawString method draws a text string that is only partially visible on the client rectangle. Yet, in the rotation example at the beginning of this chapter, we managed to rotate the letter ‘A' about a point other than the origin-in such a way that it didn't get rotated out of the window. To achieve this, we cheated a little: in fact, the letter ‘A' example was both translated and rotated. This brings us neatly onto the topic of combining different transformations.

Composite Transformations

As you may expect, there is no technical problem with the notion of carrying out any number of translations one-after-another, in such a way that they accumulate into one (potentially very complex) world transformation. However, if you don't think about the accumulation of transformations carefully, you may get some unexpected results.

One point to note is that transformations are not commutative (to use the mathematical term). This means that the order in which you apply two transformations is important.

For example, if we apply a translation and then a rotation, the result we get will not be the same as if we first apply the rotation and then the translation. Again, we'll show this by combining a code snippet with an annotated screenshot. Firstly we'll show an X-axis translation followed by a 45° clockwise rotation:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Font f = new Font("Times New Roman", 24);

    //Draw text to the surface
    g.DrawString("Translate then Rotate", f, Brushes.Black, 0, 0);

    //Translate the text 150 pixels horizontally
    g.TranslateTransform(150, 0);

    //Rotate the text 45 degrees clockwise
    g.RotateTransform(45);

    //Draw the transformed text
    g.DrawString("Translate then Rotate ", f, Brushes. Black, 0, 0);
}

The origin of the drawing surface is shifted horizontally then the axes against which the operation is oriented are rotated:

Click To expand

Now let's perform the same transformations in the reverse order, by reversing the order of the transformation calls:

    //Draw text to the surface
    g.DrawString("Rotate then Translate", f, Brushes.Black, 0, 0);

    //Rotate the text 45 degrees clockwise
    g.RotateTransform(45);

    //Translate the text 150 pixels horizontally
    g.TranslateTransform(150, 0);

    //Draw the transformed text
    g.DrawString("Rotate then Translate ", f, Brushes.Black, 0, 0);

Then we get a different result as the translation takes place along a rotated axis.

Click To expand

The result is clearly different, and demonstrates an issue that you need to be aware of when you're accumulating transformations.

Just to round off this section, let's produce a small example that may trigger some thoughts about custom controls.

Modify the Load event of a project as follows:

private void Form1_Load(object sender, System.EventArgs e)
{
    this.ResizeRedraw = true;
}

Add a Paint event, and modify like so:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Font f = new Font("Times New Roman", 16);

    for (float angle = 0; angle < 360; angle += 45)
    {
        g.ResetTransform();
        g.TranslateTransform(ClientRectangle.Width/2,
                             ClientRectangle.Height/2);
        g.RotateTransform(angle);
        g.DrawString("Hello, World", f, Brushes.Black, 50, 0);
    }
}

Each time the code in the loop is executed, the transformation is reset, a horizontal and vertical translation are performed, followed by a rotation. When we run the example, we should see the following:

Click To expand

An Aside-Transformations and Matrices

As we said at the beginning of the chapter, the convenience methods we've just been playing with obscure the use of the Matrix class. It is worth taking a little time to elaborate on the subject of how matrices are used in effecting transformations. Of course, if you're happy to use the convenience methods or you don't feel mathematically adventurous today, feel free to skip this section! This section is going to move quickly and will miss out some steps, but will provide some background.

As we saw under translations for the X-axis, the world transformation is:

X(Page Coordinate) = X(World Coordinate) + Delta X

Similarly, a scaling operation can be thought of like this:

X(Page Coordinate) = X(World Coordinate) * Scaling Factor

Rotations are more complex:

X(Page) =  X(World) * Cosine(Angle) + Y(World) * Sine(Angle)
Y(Page) = −X(World) * Sine(Angle)   + Y(World) * Cosine(Angle)

Cumulative transformations obviously involve working through these formulae and combining terms. Quite clearly this can get very complex very quickly and matrix algebra is the easiest way to handle the situation.

Missing out the mathematical derivation of matrix formation, we can represent the situation using the following expression. It shows how to calculate the final (transformed) X and Y coordinates of a point from its original coordinates, by applying matrix multiplication:

Click To expand

Here, X0 and Y0 represent the original coordinates, the 3×3 matrix contains all the information necessary for carrying out a world transformation and XT and YT are the page coordinates after transformation.

We've used matrix multiplication here. In matrix multiplication, the formula for calculating XT in this expression is XT=(X0×L1)+(X×L3)+(X0×T1). YT is calculated using a similar formula involving the second column of the matrix, and the third coordinate in the result, 1 is calculated using the third column of the matrix.

Within the matrix the Li variables represent the linear part of the transformation (incorporate the information necessary for scaling and rotation) while the Ti. variables contain any information required for a translation. Note that the 1 in the 3×1 matrix is a dummy coordinate, which enables the translation component to be included in one matrix. Since in GDI+ we are only interested in affine transformations the third column contains the numbers [0 0 1].

As we saw in Chapter 5, an identity matrix (which represents a transformation that has no effect) has ls down the leading diagonal and 0s everywhere else, like this:

Now, when a new Graphics class is used the world transform in effect uses a transform in which the identity matrix is used. In other words, the page coordinates and the world coordinates match. Moreover, the ResetTransform method resets the transformation to the identity matrix and thus clears all operative world transformations.

Indeed, the Transform property of the Graphics class is a Matrix object and is set by use of the convenience methods we've discussed. This property can be set directly, but doing so lies outside the scope of this chapter.

Just to finish this little aside let's consider what happens if we use negative values in a scaling operation. The title of the next section might give you a clue.

Reflection Transformations

Consider the following code:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, this.ClientRectangle);
    Font f = new Font("Times New Roman", 24);

    //Translate the text 175 pixels horizontally and 50 vertically
    //and draw text to the surface
    g.TranslateTransform(175, 50);
    g.DrawString("Reflection", f, Brushes.Black, 0, 0);

    // Scale the text using a negative value
    g.ScaleTransform(−1, 1);

    //Draw the translated text
    g.DrawString("Reflection", f, Brushes.Black, 0, 0);
}

The translation is carried out for convenience here, and translates the origin to the point (175, 50). In particular, note that the translation moves the Y-axis 175 pixels to the right of its original position. Now, the transformation matrix for the scaling is:

Thus our transformation can be represented by:

Click To expand

So while the Y-axis position remains constant, the X-axis values are negated and thus the transformed text is written from right-to-left. The effect is a reflection in the Y-axis:

Click To expand

Shearing Transformations

When an airplane encounters wind shear, it is descending or ascending through an altitude where the wind-speed above the airplane is different from that below it. In this situation, the airplane experiences a radical increase or decrease in lift, and we get a big bump or drop.

A shear transform is one where the ‘wind' at the top of the drawing surface is blowing the pixels at a different rate from the ‘wind' at the bottom of the drawing surface. We get an effect where the graphics that we draw ‘lean forward' or ‘lean backwards'.

A Simple Example of Shearing

This transformation is not a common one, so there is no convenience method in the Graphics class to make it easy for us to do it. However, there is a convenience method in the Matrix class. This class is in the System.Drawing.Drawing2D namespace, so when trying this example the first thing to do is add this to your example file:

using System.Drawing.Drawing2D;

Then modify the Paint event, as follows:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.FillRectangle(Brushes.White, ClientRectangle);

    Matrix m = new Matrix();

    // the following method call sets up the shear in the matrix.
    // The first argument is the shear factor in the X direction.
    // The second argument is the shear factor in the Y direction.
    m.Shear(.6f, 0);
    g.DrawRectangle(Pens.Black, 10, 10, 50, 50);
    g.MultiplyTransform(m);
    g.DrawRectangle(Pens.Black, 70, 10, 50, 50);
}

When this is run, the output looks like this:

Click To expand

The shear method effectively sets up a matrix of the form:

This gives a transformation of the form:

Click To expand

Thus the X-coordinate of a point after a shear transformation is dependent on its original X-coordinate and also on its original distance from the Y-axis-the greater the distance, the greater the X-direction shift.

This little piece of code introduces us to the outstanding MultiplyTransform method. This method appends a transformation Matrix object to the world transformation. In this case the existing transformation is in fact the identity matrix, so this method is a convenient way of allowing the Matrix object created to provide the world transformation.

Note the final DrawRectangle call merely draws the rectangle in a different position for convenience.


Invalidation and Clipping

We should note that transformations are not applied to invalidation or clipping operations. These operations always happen in terms of device coordinates, not page coordinates or world coordinates.

Another way to explain this dynamic is as follows:

  • The Graphics object that is our drawing surface is created anew with each raising of the Paint event. We set transformations by calling methods on the Graphics object, and if we want to apply a particular transformation over multiple Paint events, we have to retain that transformation as part of the state of our window, and re-apply it each time our Paint event handler executes. Transformation state is not retained from Paint event to Paint event.

  • We often need to invalidate rectangles and regions in an event handler other than the Paint event handler, such as a mouse event handler. In this case, the Graphics object that is our drawing surface hasn't even been instantiated yet.

  • Invalidating or clipping to a partial pixel doesn't really make sense, and if we were invalidating or clipping with world coordinates, we would often be doing this.

The rectangles or region that we specify in a call to an Invalidate method are not transformed, and it is not possible to transform them. After invalidating, we will receive a Paint event, along with a Graphics object. We can apply transformations to the drawing surface, but the invalidated region does not change as we apply transformations.


Transformations and Custom Controls

In Chapter 13, we'll revisit the subject of transformations in the context of building a scrolling control. However, it's fair to say that when building grid controls, textbox controls, button controls, slider controls, bar charts, line graphs, and the like, transformations aren't commonly used.

Having said that, here are a few areas in which we may reach for a transformation:

  • If we are building a charting control that has the capability of panning in any direction, we could use the translation transformation so that we would not need to change the coordinates of the location where we draw the charting elements.

  • If we are building a drawing custom control that gives us the capability to zoom in or out, we could use the scaling transformation to change the view of the custom control.

  • In many cases, the print preview capability that comes with the .NET Framework is more than satisfactory for our purposes. However, if we have some additional requirements that necessitate building our own print preview dialog box, we could use transformations to implement the panning and zooming functionality.

  • If we were building a clock custom control, or perhaps a temperature dial custom control, we might use the rotate transformation.


Summary

In this chapter, we saw how to apply global transformations, which convert from world coordinates to page coordinates. Behind the scenes, global transformations are applied via a matrix, but for the purposes of building custom controls, we don't need to concern ourselves too much with the matrix. Rather, we can use some convenience methods to easily set up our transformations.

To summarize, when we do a drawing operation, the coordinates that we pass to the drawing operation are floating point, expressed in the world coordinate system. GDI+ then applies any world transformations to the drawing operation. Then, if the page coordinate system is set to anything other than the Pixel unit of measure, then GDI+ does the conversion to pixels. This is a scaling operation based on the page coordinate system unit of measure, and the drawing surface resolution (DPI).

It is important to note that transformations don't apply to clipping invalidations. When we invalidate a rectangle (or a region), we specify coordinates in terms of pixels on the underlying drawing surface. They are expressed in device coordinates.

Specifically in this chapter we saw how to carry out transformations that allowed scaling, translating, and rotating. Additionally we illustrated how these operations can be used (carefully and in the appropriate order) to give cumulative effects. We also touched on the underlying matrix multiplication aspects of transformations and used this to aid our understanding of shearing transforms.

Finally we highlighted some areas where transformations may be of use in custom control development.

In the next chapter, we're going to move on to the subject of printing-and the similarities and differences we encounter when sending a graphic to a printer instead of sending it to the screen.


Chapter 9: Printing

Overview

It's very common for applications to provide some facility to print some data or results to a printer-and in many cases this is a crucial part of the application. Imagine, for example, trying to market a word processor or a spreadsheet that had no printing capabilities-how many copies do you think you'd sell?

We'll begin this chapter by looking at how we write code that prints to the printer. So far in this book, our examples have been focused on drawing to the screen. As it turns out, the general principles of drawing to a printer are the same, to an extent-we still use a Graphics object to represent the drawing surface and use its methods and properties to perform drawing operations. But of course, there are many differences between the screen and the printer. In this chapter we will highlight those differences and how we handle them. As we go, we'll include study of:

  • How we deal with our printer's capabilities-in terms of resolution and color-and the physical set-up of the page

  • How to allow our application's users to select various printer and page options, just as they would do when printing a word-processed document or spreadsheet

  • How to control multi-page printing

When drawing custom controls for display on the screen, we are concerned about making sure that pixels are colored exactly as we want them to be. As we've said in earlier chapters, this is particularly important for controls with horizontal and vertical lines-we don't want to create a situation where a line overshoots or falls just short of a line perpendicular to it. When drawing to a printer, we're a little less concerned about this-because printers tend to have higher resolution than screens-and so this chapter will focus less on the details of how to draw individual items, and more on how to manipulate and write handlers for the various printing-related events.

This chapter will nevertheless focus primarily on the use of Windows Forms and GDI+ for printing. There is a lot of printing-related functionality offered by Windows that doesn't have much to do with GDI+ (such as how the user selects a range of pages to print, setting the paper tray or bin, putting up status bars while printing, etc.). This chapter will focus primarily on the parts of the API that relate to using the printer as a drawing surface. We will explore the parts of the programming interface that allow our code to affect the drawing surface-such as by changing its dimensions, orientation, or resolution, getting the printable area of the drawing surface, and the like.

In addition, we'll see how to write a single set of code that draws what we want on both screens and printed pages, and we'll see what's involved in creating a print preview on the screen.

Our first example is probably the simplest printing program possible-a program that displays a string and a rectangle, and allows the user to print these on a single page. We'll then extend the sample by having it spread its printout over two pages. Next we'll examine graphics units and develop an application that uses graphics units to draw a ruler of the correct size on paper. Finally we'll examine the metrics of the drawing surface for a printer and for the Print Preview dialog.

In the course of all this, we will also see examples of the programmatic manipulation of the rudimentary features of the Print Dialog, so that we know how to put up a dialog that allows the user to select a printer, select pages to print, etc. Basic operation of the Print Dialog box (from a user perspective), and its myriad of options, is straightforward-and not within the scope of this book.

We'll start off by recapping what the user normally expects to happen when they elect to print a document.


The Printing Process

Before we examine how to take programmatic control of printing capabilities, it's worthwhile recapping the features that the user will normally expect to see, and giving a quick overview of the classes that are available to support these features. Typically, in an application that supports printing there are three menu items that the user can select:

  • Page Setup. When the user selects this option, the application presents a Page Setup dialog box to the user. This dialog allows the user to select various options (such as margin size, whether to print in landscape or portrait mode, and so on). The module that puts up the page setup dialog box retains the results of the dialog box in a PageSettings object.

  • Print. When the user selects this option, the application presents a printer selection dialog box to the user. The user selects the desired printer and presses OK. The application then starts the printing process, using the page settings.

  • Print Preview. When the user selects this option, the application opens the Print Preview dialog box, using whatever default or custom page settings have been applied.

If your application is to support printing, then you will normally want it to conform to user expectations. For that you will need to provide these three menu options. Fortunately, Microsoft has made this task easy for us by providing a number of .NET Framework classes that implement much of the boilerplate code associated with these tasks. These classes are able to store printer settings, display the standard page set up and print dialogs, and show the print preview. Normally, using these classes, the only thing that you have to implement yourself is the code that tells the computer exactly what to print.

The Printing-related Classes in GDI+

Most of the printing-related classes are in the System.Drawing.Printing namespace, and are implemented in the System.Drawing.dll assembly. However, note that some classes that represent dialogs or forms are in the System.Windows.Forms namespace and the System.Windows.Forms.dll assembly. There are quite a few such classes, but we'll briefly review the main ones next.

Before we get into our first example, we should pick out the classes in GDI+ that are of particular interest to us when we're thinking about printing. There are a number of classes that we will use when writing code to print. It is useful to list the main classes that we will use, and to understand their purpose.

There are six important classes that you'll need to be aware of. Three of them are dialogs and contained in the System.Windows.Forms namespace:

Class | Description
PageSetupDialog | Represents the Page Setup dialog. Contains properties that give us programmatic control of margins and page orientation, and various page settings and printer settings (see the PageSettings and PrinterSettings classes below).
PrintDialog | Represents the Print dialog, which is displayed immediately before printing. Contains properties that give us programmatic control of printer settings (see the PrinterSettings class below) and various other printing capabilities.
PrintPreviewDialog | Represents the Print Preview dialog. Gives us programmatic control of print preview functionality, such as zooming in and out, scrolling, etc.

The other three control the printing process itself, and belong to the System.Drawing.Printing namespace:

Class

Description

PageSettings

Encapsulates characteristics that relate to a single printed page, and which may change from page to page. This is a child of the PageSetupDialog class, because we tend to use that dialog to control page properties.

PrinterSettings

Encapsulates some characteristics of a specified printer, such as whether the printer is a color printer, whether it supports landscape mode, and so on. This is a child of the PageSetupDialog and PrintDialog classes, because we tend to use those dialogs to control page properties.

Unless we are doing a special implementation of printing (such as forcing output to a particular printer), we don't need to use this class.

PrintDocument

Encapsulates a print document in memory. We use this object to send output to a printer or print preview dialog.

How is a Document Printed or Print-previewed?

In this section we'll review what actually happens-as far as our program is concerned-when the user opts to print or print preview. We'll cover printing first.

What Happens When a User Elects to Print

As we just mentioned, the printing process itself is normally controlled with a PrintDocument class. In our code, we can invoke the printing process (that is, send the document to the printer for printing) by calling the PrintDocument.Print method-this is what we would do if the user has just clicked OK in the Print dialog. The Print method will control the printing by raising a number of events-our application will need event handlers for some or all of these events. The sequence of events is as follows:

  1. First, the BeginPrint event is raised. Our application's BeginPrint event handler should perform any special processing required when printing starts.

  2. Then the QueryPageSettings event is raised. The event handler for this event should make any changes required to printer settings that relate to just that page.

  3. Next, the PrintPage event is called. This is arguably the most important of all of these events. The handler for this event should do the actual printing of a page.

    The PrintPage event is very similar to the more familiar Paint event. There are two important differences. First, while the Paint event handler is passed an argument of type PaintEventArgs, the PrintPage event handler expects an argument of type PrintPageEventArgs. This PrintPageEventArgs object contains extra information about the page to be printed.

    Second, the Graphics object associated with the PrintPageEventArgs object will have been initialized to point to the printer. Thus, the Graphics object represents a printer drawing surface, instead of a screen drawing surface.

    In addition to printing, our PrintPage event handler should also set the PrintPageEventArgs.HasMorePages property to true or false depending whether any pages remain to be printed.

  4. Then, we loop. If PrintPageEventArgs.HasMorePages is set to true (on return from the PrintPage event handler), then the PrintDocument class will go back to raising the QueryPageSettings event (Step 2) followed by the PrintPage event (Step 3). This loop will continue until HasMorePages is set to false. Clearly this all means that your code will need to independently keep track of how many times PrintPage has been called in order to determine which page is being printed, and customize your printing code appropriately.

  5. Finally, the EndPrint event is called to perform any cleanup needed when printing has finished.

In this chapter, we will mostly use the PrintPage event. Later in the chapter, we will include an example that involves an event handler for QueryPageSettings, but we won't use either BeginPrint or EndPrint. Nevertheless, it's useful to know that these events are there if you need them.

What Happens When a the User Elects to Print Preview

In fact, the process for print previewing is very similar to the process for printing that we've described above. There are one or two differences, and we'll note them here.

The PrintPreviewDialog has a Document property, and we set this property to refer to the PrintDocument object that controls printing in our application.

In the case of print previewing, the process is invoked by a call to the PrintPreviewDialog object's ShowDialog method-which results in the Print Preview dialog being displayed. (By contrast, in the case of printing, we use the PrintDialog object's ShowDialog method to show the Print dialog and then the printing process itself begins when the user clicks OK on this dialog to invoke the PrintDocument.Print method.)

Internally, the PrintPreviewDialog.ShowDialog method will cause the same sequence of events to be raised against the contained PrintDocument object. However, it will ensure that the Graphics object is initialized to send output to the print preview window rather than to the printer. This means that you can normally reuse exactly the same drawing code for both printing and print previewing.

Using the Printing Classes

Now we've summarized the printing process from a programmatic perspective, we are in a position to see how we can use the main printing classes. Normally, we'll use these classes in roughly the following way:

  • In the class that implements the event handlers for the menu items (often the main Form class), we declare a PageSettings member variable. This variable will store the current page settings for the application.

  • The event handler for the Page Setup menu item first creates a PageSetupDialog object to represent the dialog. We assign the object's PageSettings property to be our PageSettings member variable and hence use the stored page settings in the dialog.

    We show the dialog by calling its ShowDialog method.

  • The event handler for the Print menu item first creates a PrintDocument object to represent the dialog. Then it registers one or more event handlers with the object (at the least, an event handler must be registered for the PrintPage event).

    We assign the PrintDocument object's DefaultPageSettings property to be our PageSettings member variable, so that (to begin) the dialog uses the page settings stored in state.

    The PrintPage event handler then puts up a Print Dialog box using the PrintDialog.ShowDialog method, and if the user presses OK in the dialog box, starts the printing process.

In this chapter, we'll explore the PrintDialog class in detail.

  • In a very similar fashion, the event handler for the Print Preview menu item creates an instance of the PrintDocument class, and registers one or more event handlers with the object (again, at the least, it must register an event handler for the PrintPage event).

    Again, we assign the PrintDocument object's DefaultPageSettings property to be our PageSettings member variable, so that (to begin) the dialog uses the page settings stored in state.

    The PrintPage event handler then puts up a Print Preview Dialog box using the PrintPreviewDialog.ShowDialog method.

  • As we've discussed, the PrintPage event handler will get a Graphics object as an argument to the method, and can draw on the drawing surface using this object. The .NET Framework will raise this event when it is ready for the event to draw on the printed page. Before returning, this event handler can determine if there are more pages to be printed, and indicate this to its caller. If it indicates that there are more pages, the PrintPage event will be raised again. If it indicates that there are no more pages, the printing process terminates, and PrintPage will not be raised again.

  • The print preview process uses the same events as the print process. If we write our application so that our code behaves properly regardless of the resolution of the drawing surface, our printing code will also serve for the print preview functionality.

A Simple Printing Example

Now we've met the six classes in GDI+ that provide our printing functionality set, we'll build up a little application to see them in action. Our application, called SimplePrintingExample, will simply display some text and a box as shown:

Click To expand

However what is new is that this application also enables the user to print the contents of the form to a printer, and view it in a Print Preview dialog.

To create the sample, we create a C# Windows Application in Visual Studio .NET. In the InitializeComponent routine (or via the Design view if you prefer), set the background color of the form to the color SystemColors.Window. On most machines this will appear as white (though it may be different if you have set up a non-default color scheme). You can also set the Window text here if you like:

private void InitializeComponent()
{
    ...
    this.BackColor = System.Drawing.SystemColors.Window;
    this.Text = "SimplePrintingExample";
}

Now add an event handler for the form's Paint event, and add the following code to it:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    PaintDocument(g);
}

The painting itself is done in a new helper method called PaintDocument, which we'll define next. The reason for factoring this code out into a different method is so that we can call it both when printing and when painting to the screen:

private void PaintDocument(Graphics g)
{
    g.PageUnit = GraphicsUnit.Point;

    // Draw some items to the drawing surface
    g.DrawString("Simple Printing Sample",
                 this.mainTextFont,
                 Brushes.Black,
                 new Rectangle(10, 10, 180, 30));
    g.DrawString("This text and the box appear on the screen " +
                 "and can be printed too!",
                 this.subTextFont,
                 Brushes.Black,
                 new Rectangle(30, 40, 150, 50));
    g.DrawRectangle(Pens.Blue,
                    new Rectangle(new Point(10, 90), new Size(100, 50)));
}

As you can see, this code simply draws the two text strings and the rectangle. It requires two Font member variables of the Form class, which we'll also define:

public class Form1 : System.Windows.Forms.Form
{
    private Font mainTextFont = new Font("Times New Roman", 14);
    private Font subTextFont = new Font("Times New Roman", 12);

The only other point worth noting is that we have changed the page unit in our PaintDocument routine above:

g.PageUnit = GraphicsUnit.Point;

Setting this property to GraphicsUnit.Point, means that measurements will be made in printing points (1/72 inch). The default is to make all measurements in pixels, but if we do that then the rectangle will almost certainly appear too small when we print it because a pixel on a printer occupies a far smaller area than a pixel on the screen. By setting the page unit to Point we will ensure that the unit of measurement remains similar when we swap from screen to printer. Don't worry about this too much for now-we'll explore page units in more detail later in the chapter.

Adding Printing-Event and Menu Handlers

With the code changes as above, the SimplePrintingExample program will run and display on the screen correctly. However, it doesn't yet have any printing functionality, so we need to add that next. First we need to reference the appropriate namespace in the code.

using System;
using System.Drawing;
using System.Drawing.Printing;

Next use the Visual Studio .NET design view editor to add a File menu with Page Setup, Print Preview, and Print menu options:

Click To expand

Use the Properties window for these items to give them the names menuFilePageSetup, menuFilePrintPreview, and menuFilePrint.

Also add event handlers for these menu items. We'll give these handlers the respective names menuFilePageSetup_Click, menuFilePrintPreview_Click, and menuFilePrint_Click. Now let's build in the code for these three event handlers.

The File\Page Setup Menu Event Handler

Let's start by looking at the Page Setup event handler. Recall that this needs to display a standard page setup dialog, and save any preferences the user chooses. First we need a new member variable (a PageSettings object) in the Form1 class to hold the saved settings, as we discussed earlier:

public class Form1 : System.Windows.Forms.Form
{
    private Font mainTextFont = new Font("Times New Roman", 14);
    private Font subTextFont = new Font("Times New Roman", 12);
    private PageSettings storedPageSettings;

Now let's move on to the code for the event handler.

private void menuFilePageSetup_Click(object sender, System.EventArgs e)
{
    try
    {
        PageSetupDialog psDlg = new PageSetupDialog();

        // create a PageSettings object if never before created
        if ( this.storedPageSettings == null )
            this.storedPageSettings = new PageSettings();

        // put up the dialog
        psDlg.PageSettings = this.storedPageSettings;
        psDlg.ShowDialog();

    }
    catch(Exception ex)
    {
        MessageBox.Show( ex.Message );
    }
}

This code should be fairly self-explanatory. We instantiate a PageSetupDialog object, and set its PageSettings property to refer to the stored settings. Then we call its ShowDialog method to put the dialog on the screen. This is literally all we need to do.

At this stage we can run the program and check that this works. Select the Page Setup option in the menu, and you should see the Page Setup dialog:

Click To expand

This screenshot shows the default settings. (Note that the sample page shown in the dialog is a fixed schematic supplied by Microsoft-it's not drawn using any of your code.)

The File|Print Event Handler

Next we'll see how to implement the Print menu handler.

private void menuFilePrint_Click(object sender, System.EventArgs e)
{
    try
    {
        PrintDocument pd = new PrintDocument();
        pd.PrintPage +=
                new PrintPageEventHandler( this.PrintPageEventHandler );

        // if the user has set page settings, then set the property
        // in the print document
        if ( this.storedPageSettings != null )
            pd.DefaultPageSettings = this.storedPageSettings ;
        PrintDialog dlg = new PrintDialog();
        dlg.Document = pd;
        DialogResult result = dlg.ShowDialog();
        if (result == System.Windows.Forms.DialogResult.OK)
            pd.Print();
    }
    catch ( Exception ex )
    {
        MessageBox.Show( ex.Message );
    }
}

Let's have a look at this event handler. First, we instantiate a PrintDocument object that will control the printing. We attach the handler responsible for printing each page to the PrintDocument's PrintPage event-in this case, the handler is also called PrintPageEventHandler. (We haven't built that yet-we'll do so in minute.)

Next, we set the PrintDocument's DefaultPageSettings property to refer to our storedPageSettings member object that holds any settings the user has stored. Then we instantiate a PrintDialog object, set it to refer to the newly created PrintDocument, and call its ShowDialog method. That will result in the normal Print dialog being shown.

When the user clicks OK on the Print dialog, the document will be printed. During the printing, the system will repeatedly raise the PrintPage event, so we need to code up the corresponding event handler (our PrintPageEventHandler routine). Here's what the code looks like.

protected void PrintPageEventHandler( Object obj, PrintPageEventArgs ev )
{
    Graphics g = ev.Graphics;
    PaintDocument(g);
    ev.HasMorePages = false;
}

Note that this event handler takes a PrintPageEventArgs as a parameter-just as we mentioned earlier. This class is similar to PaintEventArgs, but contains additional information related to printing. Of most immediate interest to us is the Boolean property HasMorePages, which we described earlier and which is used to determine whether the PrintPage event will be raised again. For this particular document, there is only one page to be printed, so we set this property to false.

Other than that, this event handler looks pretty much like our original Paint event handler-it calls the PaintDocument method to display the text and rectangle. The real magic of this is that the system will have ensured that the Graphics object supplied to the event handler will have been set up to direct all painting to the printer instead of the screen. So we can just call the PaintDocument method without worrying about where the result of the Paint operation will go.

One point to note is that in this simple example, we've used the same routine, PaintDocument, to print and to paint. We have been able to do this because our application is so simple: in particular, it only has one page, and we wanted the printed document to look the same as the form on the screen. In general, you'll need to take care because there will be differences. In particular, for longer documents, when painting to the screen you use scrollbars to just print the appropriate portion, while when printing you simply print one page at a time. These differences may mean that you need to write separate methods to paint to the screen and to print.

The Print Dialog

The Print dialog that gets put up looks like this.

Click To expand

The appearance of this dialog should be self-explanatory. Note that, by default, the option for the user to print only a selection from the document is disabled. For our sample it makes sense to leave it that way, because there is only one page anyway, and no facility for the user to select a region on the screen. If you wish, you can enable these controls if required by respectively setting the PrintDialog.AllowSomePages and PrintDialog.AllowSelection properties to true before putting up the dialog.

The File\Print Preview Event Handler

Finally let's look at the PrintPreview event handler:

private void menuFilePrintPreview_Click(object sender, System.EventArgs e)
{
    try
    {
        PrintDocument pd = new PrintDocument();
        pd.PrintPage +=
                new PrintPageEventHandler( this.PrintPageEventHandler );

        // if the user has set page settings, then set the property
        // in the print document
        if ( this.storedPageSettings != null )
            pd.DefaultPageSettings = this.storedPageSettings ;
        PrintPreviewDialog dlg = new PrintPreviewDialog();
        dlg.Document = pd;
        dlg.ShowDialog();
        // no separate command to print.
        //Preview dialog shows the print preview
    }
    catch ( Exception ex )
    {
        MessageBox.Show( ex. Message );
    }
}

As you can see, this event handler is very similar to the Print event handler, so we've simply highlighted the two differences. We instantiate a PrintPreviewDialog object (instead of a PrintDialog object), and we send the document straight to the Print Preview dialog via its ShowDialog method (instead of waiting for the result of a Print dialog and then sending the document to the printer).

The Print Preview Dialog

When we open the Print Preview dialog box, we see a preview of our document. The PrintPreviewDialog class that has been supplied by Microsoft is very sophisticated. We don't need to do any programming ourselves; it provides for the user to be able to take several actions, including:

  • Print directly to the printer from the Print Preview dialog box

  • Zoom in or out on our document

  • Pan around our document, if zoomed in close enough

  • Select the number of pages that are shown simultaneously (1, 2, 3, 4, or 6). There are five buttons at the top of the dialog for this, as you can see in the following screenshot. Note that if you select to display more pages than are actually in the document, then the dialog will just display the pages that are available

  • Go directly to a specific page.

The Print Preview dialog box looks like this:

Click To expand

Printing to More than One Page

The SimplePrintingExample program demonstrates how events related to printing are raised, and how you can print and print preview by supplying the event handler for the PrintPage event. However, there is one respect in which it is too simplistic: it only prints onto one page. In the real world, it is far more likely that your application's printout will take up several pages.

Therefore, in this section we will develop SimplePrintingExample a bit further, so that its output is spread over two pages, thus illustrating how you can control multi-page printing. To achieve this we make the following changes to the sample code, now renamed MultiPagePrintingExample.

First we add a member variable to the Form1 class. This variable will store the count of which page we are up to in any printing job:

public class Form1 : System.Windows.Forms.Form
{
    private Font mainTextFont = new Font("Times New Roman", 14);
    private Font subTextFont = new Font("Times New Roman", 12);
    private PageSettings storedPageSettings;
    uint currentPage;

In the event handler for the File | Print menu option click event, we initialize this variable to indicate Page 1:

private void menuFilePrint_Click(object sender, System.EventArgs e)
{
    try
    {
        this.currentPage = 1;
        PrintDocument pd = new PrintDocument();
        pd.PrintPage +=
                new PrintPageEventHandler( this.PrintPageEventHandler );

        // if the user has set page settings, then set the property
        // in the print document
        if ( this.storedPageSettings != null )
            pd.DefaultPageSettings = this.storedPageSettings ;
        PrintDialog dlg = new PrintDialog();
        dlg.AllowSomePages = true;
        dlg.Document = pd;
        DialogResult result = dlg.ShowDialog();
        if (result == System.Windows.Forms.DialogResult.OK)
            pd.Print();
    }
    catch ( Exception ex )
    {
        MessageBox.Show( ex.Message );
    }
}

Note that we've also set the AllowSomePages property of the PrintDialog object to true, to enable the user to choose to print only some pages.

We also need to initialize the page in the print preview event handler:

private void menuFilePrintPreview_Click(object sender, System.EventArgs e)
{
    try
    {
        this.currentPage = 1;
        PrintDocument pd = new PrintDocument();

Now let's look at the revised handler for the PrintPage event. Since what is being printed is no longer identical to what appears on the screen (the screen shows the text and rectangle together, but when printing these appear on separate pages), we are no longer able to combine the painting code for the two scenarios. Instead, we will need to write separate drawing code in the PrintPage event handler that detects the current page and responds appropriately. Here's how we do it:

protected void PrintPageEventHandler( Object obj, PrintPageEventArgs ev )
{
    Graphics g = ev.Graphics;
    g.PageUnit = GraphicsUnit.Point;

    switch (currentPage)
    {
        case 1:
            g.DrawString("Simple Printing Example",
                         this.mainTextFont,
                         Brushes.Black,
                         new Rectangle(10, 10, 180, 30));
            g.DrawString("This text and the box appear on the screen " +
                         "and can be printed too!",
                         this.subTextFont,
                         Brushes.Black,
                         new Rectangle(30, 40, 150, 50));
            ev.HasMorePages = true;
            ++currentPage;
            break;
        case 2:
            g.DrawRectangle(Pens.Blue,
                    new Rectangle(new Point (10, 90), new Sized(100, 50)))
            ev.HasMorePages = false;
            break;
        default:
            Debug.Assert(false);
            break;
    }
}

For the Debug.Assert method, you'll need to add a reference to the System.Diagnostics namespace:

    using System;
    using System.Drawing;
    using System.Drawing.Printing;
    using System.Diagnostics;

To appreciate what we've done here, it's worth comparing this code to the PaintDocument and PrintPageEventHandler methods of the SimplePrintingExample application. This routine contains a little code from both of those methods, plus a little new code.

We retrieve the Graphics object that will be used to print or print preview, and set the page unit to Point. What we do next is dependent on the page being printed:

  • If currentPage is 1, we draw the text and then increment currentPage so it will have the correct value the next time PrintPageEventHandler is called.

  • If currentPage is 2, we draw the rectangle, and set the HasMorePages property of PrintPageEventArgs to false, to make sure that the PrintPage event is not raised again.

Note that the PrintPageEventArgs itself doesn't store any record of the page that is to be printed. (It would have been nice if it did, because it would have saved us from having to maintain this record separately in our code.) All the PrintPageEventArgs object can do is indicate whether more pages remain to be printed.

Also note that this PrintPageEventHandler routine is still rather inflexible, in that we have determined there will be exactly two pages. In a more sophisticated application, you will need to write code that examines the data being printed at run time, to determine how many pages should be printed and what should be printed on what page. This will depend on how much data can be printed on a page, and this in turn depends on the printer settings (page size, margins, etc.). We will see how to access this data later in the chapter.

With the above changes, the MultiPagePrintingExample application works correctly, as can be seen from the Print Preview dialog. In this screenshot, we have opted to display four pages, but the program has correctly detected (by repeatedly raising the PrintPage event) that there are only two pages to be printed:

Click To expand


Selecting the Unit of Measurement

In this section we will have a closer look at measurement units. This actually forms part of a large topic, page transformations and the world coordinate system, which we first met in Chapter 8. Here we'll just examine the measurement units themselves.

By default, when painting in response to a Paint event, the graphics unit is set to ensure measurements are in pixels for drawing shapes and images (though not for text fonts, as we'll see later). So, for example, the following code will display a square that is 100 pixels by 100 pixels in size:

private void PaintDocument(Graphics g)
{
    ...
    g.PageUnit = GraphicsUnit.Pixel;           // can omit this line
                              // line as Pixel is the default anyway
    Rectangle cr = new Rectangle(new Point(10, 90), new Sized(100, 100));
    g.DrawRectangle(Pens.Blue, cr);
}

Using pixels as a measurement is often extremely convenient for displaying to the screen, because a pixel is the smallest unit that can be drawn on the screen. By specifying (for example) a line width for a horizontal or vertical line as an integer number of pixels, we know that we are specifying a line width that can be drawn exactly, resulting in a clear image on the screen that can be drawn without aliasing.

Unfortunately, when it comes to printing, measuring sizes in pixels causes problems-because a pixel is considerably smaller on a printer than it is on the screen. On the screen, the above code will result in a square with sides of approximately one inch, depending on the exact resolution of the monitor. But many printers come with resolution of over 1000 DPI (dots per inch, the number of pixels per inch). If we use the above code to print a square on the printer, the square will still have sides of 100 pixels each-but on a printer with a resolution of 1200 DPI, this will be less than 0.1 inches!

The conclusion is that if you use pixels for your measurements then it becomes a lot harder to write applications whose printouts look the same as the graphic displayed on the screen.

Fortunately, we can get round this by setting the Graphics.PageUnit property. This allows us to use measurements in some unit that remains roughly the same on the screen and printer. You can set it to any of the following values from the GraphicsUnit enumeration (which is part of the System.Drawing namespace):

Value | Meaning
Pixel | The smallest unit into which an image can be broken down. Typically this is 1/96 inch on a display device, and anything from 1/300 to 1/1000 or less on a printer.
Inch | An inch.
Display | 1/96 inch.
Point | This is a standard point as traditionally used by printers, and is equal to 1/72 inch.
Document | 1/300 inch.
World | This unit is determined by the Graphics object's World Transformation.

The following code will draw a square with sides roughly 1 inch, irrespective of whether it is drawn to the screen or the printer.

private void PaintDocument(Graphics g)
{
    ...
    g.PageUnit = GraphicsUnit.Inch;
    RectangleF cr = new RectangleF(new Point(0.4f, 0.9f),
                                   new Size(1,0f,1.0f));
    g.DrawRectangle(Pens.Blue, cr);
}

The rectangle will be located roughly 0.4 inch horizontally and 0.9 inch vertically from the top left corner. Note that because we are using inches, we need to express measurements in fractional numbers and hence need to use a RectangleF object rather than a Rectangle object.

Note that we've qualified several of the above statements with the word ‘roughly'. That's because of an unfortunate anomaly that screen measurements are not always precisely correct. By default, all screen resolutions are defined to be 96 DPI (you can check this in the Settings tab of your windows desktop Properties dialog, by clicking the Advanced button.) However, different monitors have different sizes and so don't always display 96pixels to the inch. Hence that measurement is only approximate. On the other hand, the computer is able to query a printer for its precise DPI, so measurements do normally come out exactly correct on the printed page.

Default Graphics Units

There are three separate areas in which we need to pay particular attention to the PageUnit property of the Graphics object. We need to examine them separately because the graphics units are by default set differently for some of these areas.

  • Drawing lines and filling shapes: Coordinates that we pass to drawing methods are in the units of the PageUnit property. As mentioned, the default setting for PageUnit is GraphicsUnit.Display. By default, all coordinates are expressed in 1/96 inch.

    One point that may not be immediately obvious is that pen widths are also expressed in the same units. If we use a pen width of 1, then this means that the pen width will be 1/96 inch. If our printer has a resolution of 300 DPI, the pen will be about three pixels wide.

  • Drawing text: With Font objects, the Font.Unit property is the GraphicsUnit for text drawn with the font. This means that we can use one scale for drawing lines and filling shapes, and another altogether for drawing text. Thus, we could conceivably have a method that draws some text at a position expressed in the units stored in Graphics.PageUnit, although the actual size of the text drawn is based on the units stored in Font.Unit.

    The default value for Font.Unit is GraphicsUnit.Point, which is defined to be 1/72 inch. If we let the Graphics.PageUnit property remain set to GraphicsUnit.Display, and we let the Font.Unit property remain set to GraphicsUnit.Point, we can accurately predict how our drawing operations will affect the drawing surface.

    Alternatively, we could set the Graphics.PageUnit property to GraphicsUnit.Inch, and also set the Font.Unit property to GraphicsUnit.Inch. In this case, all operations use the same unit of measure, and may simplify our drawing code. If you use this technique, there is one point to remember: pen widths will also be expressed in terms of inches. If we create a pen with a width of 1, this means that our pen will be one inch wide! However, this is easy to rectify, since we can express pen widths as floating-point numbers. If we set the pen width to 1/96, we will get the same results as if we had left Graphics.PageUnit set to GraphicsUnit.Display.

  • Drawing images: When drawing an image, we specify the destination rectangle in terms of Graphics.PageUnit. This means that GDI+ will almost certainly need to scale the image.

Drawing Using Inches

In the next section we will develop an example application, called RulerPrintingExample, which illustrates use of the Inch as the graphics measurement unit. Before we start the sample code, we'll quickly review what we need to do when using this technique.

  • We set the Graphics.PageUnit property to GraphicsUnit.Inch. We do this both when printing and when drawing to the screen.

  • We set the pen width to 1/96 inch. When printing, this has a nearly identical effect as using a pen width of 1 when the Graphics.PageUnit property is set to GraphicsUnit.Device; but because the screen is defined as having a resolution of 96 DPI, this results in creating a pen with a width of exactly one pixel. This removes any chance of aliasing; or if anti-aliasing is turned on, this removes the possibility of anti-aliasing artifacts.

The Ruler Printing Example

This example actually draws a ruler, which will appear roughly the same when drawn on a window or printed to a page. We say ‘roughly' because, as previously noted, depending on your monitor, the measurements may not be exactly correct on the screen, although they will be on the printed copy.

The code for this sample is almost identical to that for the earlier SimplePrintingExample. In fact, the only difference is in the implementation of the PaintDocument method:

private void PaintDocument( Graphics g )
{
    g.PageUnit = GraphicsUnit.Inch;
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    float offset = .75f;
    Pen p = new Pen(Color.Black, 1.0f / 96.0f);
    for (float rule = 0; rule <= 5.0f; rule += .25f)
    {
        float x = offset + rule;
        float len;
        if (rule == (int)rule)
        {
            String s = ((int)rule).ToString();
            len = 0.5f;
            System.Drawing.Font f =
                  new Font( "Times New Roman", 0.25f, GraphicsUnit.Inch);
            StringFormat sf = new StringFormat();
            sf.Alignment = StringAlignment.Center;
            RectangleF mr = new RectangleF(x − .25f, 1.5f, .5f, f.Height);
            g.DrawString( s, f, Brushes.Black, mr, sf );
        }
        else if (rule * 2 == (int)(rule * 2))
            len = 0.375f;
        else
            len = 0.25f;
        g.DrawLine(p, x, .75f, x, .75f + len);
    }
}

Running the RulerPrintingExample application results in the following:

Click To expand

The print preview results in this:

Click To expand


Printing Metrics

We use the term printing metrics to describe the data that determines how the printer actually prints a document. It includes such measurements as the printer resolution (that DPI value we've talked about), and the paper size and orientation.

It is sometimes important be able to read certain metrics within the code that prepares our document for printing or print previewing. We can use these metrics to determine how we are going to print the contents of the page. For example, if we are writing code that is designed to print large spreadsheets or documents, we will need to use the paper size to calculate exactly how much text we can fit onto a page, and hence where the page breaks should occur and how many pages we need to print.

Therefore, before we make a small application to print out some metrics, let's define them. We'll separate these metrics into two groups: those that relate to the printer, and those that relate to the printed page.

There are some metrics that can be changed in the Page Setup dialog-these are obtained from the PageSettings object. Other metrics are passed to the PrintPage event handler, and these are obtained from the PrintPageEventArgs object. There are other metrics that are obtained from the supplied Graphics object itself.

First, let's look at some metrics that relate to the printer itself. The following table lists some important metrics. We've also mentioned the classes that these properties belong to.

Metric

Description

PageSettings.PaperSize

This property returns a PaperSize object, whose properties (Height, Width, Kind, PaperName) tell us about the type of paper that the printer says it will print to.

The dimensions (Height and Width) are in units of 1/100 inch. The Height property refers to the long side of the paper (therefore, these dimensions are not affected by changing the page orientation between Portrait and Landscape).

PageSettings.PrinterResolution

This property returns a PrinterResolution object, whose properties (Kind, X, Y) describe the resolution of the printer. The Kind property returns a value from the PrinterResolutionKind enumeration (High, Medium, Low, Custom, Draft), and the X and Y properties are integers.

PageSettings.PrinterSettings

.LandscapeAngle

If the printer supports landscape printing, then this value represents the angle of rotation that the portrait orientation is rotated in order to get the landscape orientation. In this case, the value is either 90 or 270 (degrees). (Note that the printer will print portrait mode unless the PageSettings.Landscape property is set to true.)

If the printer does not support landscape printing, then this value is 0.

Now, we'll look at some metrics about pages to be printed on the printer:

Metric | Description
PageSettings.Bounds | This is a Rectangle object that reflects the dimensions of the drawing surface. These dimensions are always specified in 1/100 inch unit-independently of the Graphics object's PageUnit property. These dimensions are affected by a change in the page orientation.
PrintPageEventArgs.PageBounds | This metric is the same as PageSettings.Bounds, and is supplied for convenience only.
PageSettings.Margins | This property returns a Margins object, whose properties (Top, Bottom, Left, Right) report the margins that the user has specified in the Page Setup dialog box (in 1/100 inch units). If possible, we should honor this setting. See next entry.
PrintPageEventArgs.MarginBounds | This is a Rectangle object that represents the area of the page within the margins. It is pre-computed for our convenience by taking the value of PageSettings.Bounds and decrementing the dimensions by the values in PageSettings.Margins.
Graphics.DpiX | Reports the physical horizontal resolution of the drawing surface.
Graphics.DpiY | Reports the physical vertical resolution of the drawing surface.
Graphics.VisibleClipBounds | This is a RectangleF object that reports the drawable area of the drawing surface. Some printers don't allow us to draw right up to the edge of the drawing surface, due to physical constraints of the printer. There is an issue that you should be aware of when using this metric. See the next subsection.

Displaying Printing Metrics

We will now write an example, called PrintingMetricsExample, that writes out the main printing metrics to the console window. The easiest place from which to access the printing metrics is from within the PrintPage event handler. This event handler has access to the PrintPageEventArgs object, which in turn holds all the printer settings-either directly or via the Graphics or PageSettings objects, which are supplied as properties of the PrintPageEventArgs object.

The code in this example is identical to that in the SimplePrintingExample application, except in two places. First, we modify the PrintPageEventHandler, as follows:

protected void PrintPageEventHandler( Object obj, PrintPageEventArgs ev )
{
    WriteMetricsToConsole(ev);
    Graphics g = ev.Graphics;
    PaintDocument(g);
    ev.HasMorePages = false;
}

Second, we'll add the code for the WriteMetricsToConsole method that is called here. This method does the job of writing the output to the console window. The method itself looks like this:

private void WriteMetricsToConsole(PrintPageEventArgs ev)
{
    Graphics g = ev.Graphics;
    Console.WriteLine("Information about the printer:");
    Console.WriteLine("ev.PageSettings.PaperSize: " +
                          ev.PageSettings.PaperSize);
    Console.WriteLine("ev.PageSettings.PrinterResolution: " +
                          ev.PageSettings.PrinterResolution);
    Console.WriteLine("ev.PageSettings.PrinterSettings.LandscapeAngle: " +
                          ev.PageSettings.PrinterSettings.LandscapeAngle);
    Console.WriteLine("");
    Console.WriteLine("Information about the page: ");
    Console.WriteLine("ev.PageSettings.Bounds: " + ev.PageSettings.Bounds);
    Console.WriteLine("ev.PageBounds: " + ev.PageBounds);
    Console.WriteLine("ev.PageSettings.Margins: " +
                          ev.PageSettings.Margins);
    Console.WriteLine("ev.MarginBounds: " + ev.MarginBounds);
    Console.WriteLine("Horizontal resolution: " + g.DpiX);
    Console.WriteLine("Vertical resolution: " + g.DpiY);
    g.SetClip(ev.PageBounds);
    Console.WriteLine("g.VisibleClipBounds: " + g.VisibleClipBounds);
    SizeF drawingSurfaceSize = new SizeF(
                      g.VisibleClipBounds.Width*g.DpiX/100,
                      g.VisibleClipBounds.Height*g.DpiY/100);
    Console.WriteLine("Drawing Surface Size in Pixels: " +
                      drawingSurfaceSize);
}

Keep in mind that in this example, we're only accessing the settings within the PrintPage event handler because it's convenient to do so for demonstration purposes. If you used this example to print a 20-page document, then this event handler would write the same information to the console 20 times! If we were writing production code, we would probably access information about metrics from other places-for example, within the Print or Print Preview menu event handlers, using the PrintDocument's DefaultPageSettings and PrinterSettings properties.

Note that there is an issue with the VisibleClipBounds property of the Graphics class. This property is the intersection between the clipping region of the drawing surface and the printable area of the printer. With some printers, if the clipping region has not previously been set, then this property does not contain a valid value. If you come across this problem, you can work round it by setting the clipping region to the PageBounds before accessing this property, as follows:

g.SetClip(ev.PageBounds);
Console.WriteLine("g.VisibleClipBounds:" + g.VisibleClipBounds);

To see the output, run the example and select Print or Print Preview. Then inspect the console window, which reveals the following file contents:

Information about the printer:
ev.PageSettings.PaperSize: [PaperSize A4 Kind=A4 Height=1169 Width=827]
ev.PageSettings.PrinterResolution: [PrinterResolution X=600 Y=600]
ev.PageSettings.PrinterSettings.LandscapeAngle: 90

Information about the page:
ev.PageSettings.Bounds: {X=0,Y=0,Width=827,Height=1169}
ev.PageBounds: {X=0,Y=0,Width=827,Height=1169}
ev.PageSettings.Margins: [Margins Left=100 Right=100 Top=100 Bottom=100]
ev.MarginBounds: {X=100,Y=100,Width=627,Height=969}
Horizontal resolution: 600
Vertical resolution: 600
g.VisibleClipBounds: (X=0,Y=0,Width=827,Height=1169}
Drawing Surface Size in Pixels: {Width=4962, Height=7014}

You may find that your printer and setup generates different results. It's instructive to try using the Page Setup dialog to change some settings, and then checking the print preview again to see how the output changes. For example, try changing the printer orientation from portrait to landscape mode, and see the changes in the ev.PageSettings.Bounds values. Also, try changing the margins, and look for the effects in the ev.PageSettings.Margins and ev.MarginBounds values.

Controlling the Printer Drawing Surface

In this section we'll examine how our applications can programmatically control characteristics of the drawing surface through the values of some of the printing metrics. For example, we could perform any of the following actions programmatically:

  • Change the resolution of the printer

  • Set the print quality to draft mode

  • Force the printer to print in landscape orientation

  • Force the printer to print in black and white

Before we consider some activities like this, we should have a word on order of precedence.

The Order of Precedence of Settings

In general, printing metrics and printer settings are determined in one of three ways:

  • Values set by the user

  • Values set programmatically

  • System default values

The order of the above list is significant, because it shows the expected 'order of priority' of the different 'parties' that influence these values. In other words, in normal circumstances any values set programmatically by the application will take precedence over system default values. However, any values set by the user (using the Page Setup dialog or the Print dialog) take precedence over both.

Thus, in our application we might choose to set certain metrics, if we know that the default values are not appropriate for our application (for example, if we know that our printout will look silly in portrait mode, we can programmatically override the system default of Portrait by setting landscape mode). Subsequently, once our application has displayed a Page Setup dialog, the user has a chance to override the default and programmatic settings that were in place when the application started.

When the user OKs the Page Setup dialog, the settings and the metrics specified by the user are stored in the PageSettings object. Even when that has happened, it is still possible to override those user-specified settings in your code (as we'll see in an example shortly). However, it's not always a good idea to override your user's custom settings-because that will cause your application to behave counter to your user's expectations, and will probably annoy them. If you believe that some option the user might choose is likely to be inappropriate for your application, it's far better to write code that displays a warning inviting the user to change their preferences, but then to do what the user wants.

This kind of design decision comes down to understanding your user's expectations and the requirements of the application. But with this rule-of-thumb in mind, let's see how we can change printing metrics programmatically.

Changing Printing Metrics Programmatically

All of the metric-changing activities we listed at the top of this section involve writing an event handler for the QueryPageSettings event. A QueryPageSettings event is raised before each PrintPage event is raised.

The QueryPageSettings event handler expects two arguments-one is a QueryPageSettingsEventArgs object, which contains (as one of its properties) a PageSettings object. We can change the properties of this object within the QueryPageSettings event handler.

When we do so, the values will be reflected in the subsequent PrintPage event-both in the values of its PrintPageEventArgs.PageSettings property, and in the page that it prints.

Note that we can't simply change the PageSettings in the PrintPage event. By that time, it is too late-any changes made in the PrintPage event will have no affect on the printed page. While this sounds restrictive, it does make sense: the module that raises these events needs to construct the appropriate Graphics object before raising the PrintPage event, with appropriate resolutions, appropriate page bounds, etc. This is why the QueryPageSettings event is raised before the PrintPage event-it is our chance to allow the framework to prepare for the PrintPage event.

Any changes that we make to the PageSettings from within the QueryPageSettings event are only effective for the next single page to be printed. If we want to make changes that apply over subsequent pages, we need to apply the changes again in subsequent QueryPageSettings events. Alternatively, we can make changes that apply to the entire print job-as we'll see later in the chapter.

Changing the Printer Resolution

Let's suppose (for the sake of example) that we are writing an application in which we must force the print quality to the lowest level, regardless of the print quality that the user specified. Each printer has a list of printer resolutions that are valid for that printer, and when we set the PrinterResolution property of the PageSettings object, we can only set it to one of the values in the list.

First, let's take a look at the available resolutions for a given printer. We can find out the available printer resolutions by querying the QueryPageSettingsEventArgs object's PageSettings.PrinterSettings.PrinterResolutions collection. We can do this by using something as simple as a foreach loop. To see this working, you can try this sample code-which displays output to a console window:

protected void QueryPageSettingsEventHandler( Object obj,
    QueryPageSettingsEventArgs e )
{
    ...
    foreach (PrinterResolution pr in
        e.PageSettings.PrinterSettings.PrinterResolutions)
    {
        Console.WriteLine(pr);
    }
}

If you run this fragment of code against a particular printer, you'd get output to the console window that would look like this:

[PrinterResolution High]
[PrinterResolution Medium]
[PrinterResolution Low]
[PrinterResolution Draft]
[PrinterResolution X=200 Y=200]
[PrinterResolution X=300 Y=300]
[PrinterResolution X=600 Y=600]

In order to change the printer resolution, we need to loop through the collection of possible settings, find the one that we want, and then set the PageSettings.PrinterResolution property to our desired resolution. As we mentioned earlier, the PrinterResolution class has a property called Kind-it represents the resolution of the printer and is set to one of the values in the PrinterResolutionKind enumeration:

PrinterResolutionKind Enumeration Member | Description
Custom | Custom resolution
Draft | Draft-quality resolution
High | High resolution
Low | Low resolution
Medium | Medium resolution

As we loop through the available printer resolutions, we'll test the Kind property against the appropriate member in the QueryPageSettings event handler, and hence set the printer resolution to the desired option.

To try this out, we'll build the PrintingMetricsExample2 application. This application is exactly like the PrintingMetricsExample application, but with a couple of changes that we'll describe here. First, we need a QueryPageSettings event handler like this:

protected void QueryPageSettingsEventHandler( Object obj,
    QueryPageSettingsEventArgs e )
{
    foreach (PrinterResolution pr in
        e.PageSettings.PrinterSettings.PrinterResolutions)
    {
        if (pr.Kind == PrinterResolutionKind.Low)
            e.PageSettings.PrinterResolution = pr;
        break;
    }
}

This event handler loops through the different printer resolutions until it finds one that is classified as a low-resolution setting, then it sets the resolution to this value.

Of course, we need to modify the event handlers for the menu items for printing and previewing, to register the QueryPageSettings event handler in the PrintDocument object:

private void menuFilePrint_Click(object sender, System.EventArgs e)
{
    try
    {
        PrintDocument pd = new PrintDocument();
        pd.QueryPageSettings += new QueryPageSettingsEventHandler(
                                    this.QueryPageSettingsEventHandler );
        pd.PrintPage +=
                 new PrintPageEventHandler( this.PrintPageEventHandler );

We also make the same change to the menuFilePreview_Click event handler.

Compile and run the application, and you should see the change in the printer resolution. Moreover, you can check the printer metrics output that is sent to the console window: you'll see the change reflected in the DPI settings as retrieved from the Graphics object. On my machine, doing this yielded the following, with the change highlighted:

Information about the printer:
ev.PageSettings.PaperSize: [PaperSize A4 Kind=A4 Height=1169 Width=827]
ev.PageSettings.PrinterResolution:[PrinterResolution X=200 Y=200]
ev.PageSettings.PrinterSettings.LandscapeAngle: 90

Information about the page:
ev.PageSettings.Bounds: {X=0, Y=0, Width=827, Height=1169}
ev.PageBounds: {X=0, Y=0, Width=827, Height=1169}
ev.PageSettings.Margins: [Margins Left=100 Right=100 Top=100 Bottom=100]
ev.MarginBounds: {X=100,Y=100,Width=627,Height=969}
Horizontal resolution:200
Vertical resolution:200
g.VisibleClipBounds: {X=0,Y=0,Width=827,Height=1169}
Drawing Surface Size in Pixels: {Width=4962, Height=7014}
Forcing Page Orientation

We can force page orientation to either landscape or portrait mode by using the following code in the QueryPageSettings event handler:

protected void QueryPageSettingsEventHandler( Object obj,
    QueryPageSettingsEventArgs e )
{
    ...
    e.PageSettings.Landscape = true;
}
Forcing Color Depth

We can test for color depth using the PageSettings.Color property. There are many situations in which the PageSettings.Color setting is worth investigating. For example, if our application is one that draws bar charts that make use of colors, then it's a good idea to check the PageSettings.Color property to see if the printer supports color. If it doesn't, then we can get round the problem by employing hatch brushes instead of color brushes to paint the bar chart.

In the following code, we use the QueryPageSettings event handler to force the output to be in black and white:

protected void QueryPageSettingsEventHandler( Object obj,
    QueryPageSettingsEventArgs e )
{
    ...
    e.PageSettings.Color = false;
}

Changing Metrics for the Entire Print Job

As we noted earlier, changes made within the QueryPageSettings event handler are on a page-by-page basis-in other words, the changes we made had an effect only on the page printed immediately after that QueryPageSettings event.

It's also possible to make changes that affect the entire print job. We do this by making the property assignments within the Click event handler for the Print or Print Preview menu items, after we've created the PrintDocument object. For example, in the menuFilePrint_Click event handler, we can modify the DefaultPageSettings property, as follows:

private void menuFilePrint_Click(object sender, System.EventArgs e)
{
    try
    {
        PrintDocument pd = new PrintDocument();
        pd.PrintPage +=
            new PrintPageEventHandler( this.PrintPageEventHandler );

        // if the user has set page settings, then set the property
        // in the print document.
        if ( this.storedPageSettings != null )
            pd.DefaultPageSettings = this.storedPageSettings;

        // put document-level settings here
        pd.DefaultPageSettings.Landscape = true;
        PrintDialog dlg = new PrintDialog();
        ...

This will cause the entire document to be printed in landscape mode.

We should note that any changes that we make to these DefaultPageSettings property only affect the current document being printed. These changes don't have any effect on the default settings for the printer.

Where the User Changes the Settings

We mentioned earlier that the various printing metrics can be stored in several places. To some extent, this reflects the fact that traditionally, the user has been able to set different metrics in different places-with some being set in the Page Setup dialog and others in the Print dialog. In this section we'll quickly review what settings can be set where using the Microsoft-supplied dialogs.

The Page Setup Dialog

As we've seen, the Click event handler for the Page Setup menu item puts up the Page Setup dialog box. The dialog itself is represented by a PageSetupDialog object:

PageSetupDialog psDlg = new PageSetupDialog() ;

// create a PageSettings object if never before created
if ( this.storedPageSettings == null )
    this.storedPageSettings = new PageSettings();

// put up the dialog
psDlg.PageSettings = this.storedPageSettings ;
psDlg.ShowDialog();

In this dialog, the user can set paper size, paper source, print orientation, and margins. Changes to any of these settings are reflected in the PageSettings object. Changes are made to the PageSettings object to which the QueryPageSettings event handler has access, where we can make additional changes to it. In addition, changes are reflected in the PageSettings object to which the PrintPage event handler has access.

The Print Dialog

The Click event handler for the Print menu item puts up the Print dialog box. This dialog is represented by a PrintDialog object:

PrintDialog dlg = new PrintDialog();
dlg.Document = pd;
DialogResult result = dlg.ShowDialog();
if (result == System.Windows.Forms.DialogResult.OK)
    pd.Print();

If the user clicks the Properties button for the printer, they can set paper size, graphics resolution, and print orientation. Again, changes to these settings are reflected in the PageSettings object.


Summary

In this chapter we have seen the basics of how to make your Windows Forms application support printing capabilities. In general, user expectation is for three main activities-page setup, printing, and print previewing-and in many applications we represent these three activities in the form of three standard dialogs. GDI+ provides us with objects (PageSetupDialog, PrintDialog, and PrintPreviewDialog) that represent these three dialogs, and a system of events and event handlers that we can use to react when the user selects these dialogs in our application's menu.

We saw that the general principles for drawing to a printer or print preview are the same as for drawing to the screen, to the extent that we control what gets printed by calling the appropriate methods on a Graphics object, and those methods remain the same-we still use such favourites as Graphics.DrawString and Graphics.DrawRectangle. But at a deeper level, there are many differences between the screen and a printer. Most notably, with the screen, we control the portion of the ‘document' that is displayed by using scrollbars in conjunction with the Graphics.TranslateTransform. Printers don't have scrollbars, but instead we print each page separately. Also, whereas the Paint event is raised by the operating system whenever it detects that something needs to be redrawn, printing is done in response to a specific request by the user, normally after displaying a Print dialog. Hence the sequence of events when printing is slightly different.

There are some other special considerations to make when drawing to a printed page drawing surface, which are clearly not relevant to the other drawing media. For example, we must deal with things like the printer's capabilities (in terms of resolution and color) and the physical setup of the page to be printed. The default system settings can be overridden in two ways: either programmatically in our application (using properties of the PageSettings and PrinterSettings objects that represent the print metrics) or through the user's custom settings, invoked via the aforementioned Print and Page Setup dialogs. We can design our application in such a way that programmatic settings (chosen at design time) override user settings (chosen at run time)-but this tends to go against the grain of user expectation. Ultimately, this issue is one of application design.

Another difference relates to resolution. Printers tend to have relatively much higher resolution than screens, and that means that we have to consider our unit of measurement carefully. On a 96 DPI screen, a 100-pixel line is around one inch in length; on a 600 DPI printer, the same line is less than 1/6 inch! One way round this is to set the Graphics.PageUnit property to (say) GraphicsUnit.Inch, and then draw all graphics in terms of inches-this helps to ensure a consistency of appearance between screen and printer, and gets us closer to achieving a situation in which we have a single set of code that draws what we want both on screens and on printed pages.

We covered the printing programming interface in detail, including how to create a PrintDocument object, and implement a PrintPage event handler for it. When we want to change characteristics of the print document as a whole, we can change the DefaultPageSettings property of the PrintDocument object. When we want to change the drawing surface characteristics of an individual page, we can do so by implementing the QueryPageSettings event.

There are a large number of metrics that we can get about the printer and each printed page. We saw how to get these metrics. This allows us to write graphical code that takes the page orientation, color depth, and more, into account.


Chapter 10: An Alternative Coordinate System

Overview

In Chapter 2 we talked about how we can describe our drawing surface in terms of rows and columns of pixels. We describe individual pixels in terms of a set of numbered gridlines, which run horizontally and vertically through the centers of the pixels. Thus, if we choose any vertical gridline and any horizontal gridline, they cross in exactly one point-and that point is the center of a pixel.

Having established this grid coordinate system, we looked at various drawing and filling operations and saw how GDI+ interprets these drawing operations in terms of this coordinate system (and with consideration to other things like pen width, too). Over the course of subsequent chapters, we came across one or two interesting effects that we have to deal with. For example:

  • We found that at the pixel level, we must think about drawing and filling operations quite differently. When we use DrawRectangle to draw a square whose side length is, say, 100 pixels, we found that the square drawn on the drawing surface has side length 100 pixels plus the width of the pen we draw with. Yet if we use FillRectangle to fill the same square, the result is exactly 100 pixels square.

  • We found that when we fill a square rectangle with integer pixel coordinates, with anti-aliasing switched on, GDI+ interprets this not as a whole number of pixels, but in terms of half-shaded pixels at the edges.

    This is in direct contrast to the behavior that we found in Chapters 7, when we covered regions, clipping, and invalidation. There, we saw that a region is composed of whole pixels. It's as if the region is drawn with a grid whose gridlines separate adjacent rows and columns of pixels, rather than running through the centers of the pixels.

When we're using both functions in the same graphic, and we want to control the image carefully at the pixel level, inconsistent behavior like this makes our job much more difficult. When we're working on a screen resolution of around 96 DPI, we want to be able to control the exact color of each pixel as closely as possible. But the fact is that the same dimensions can produce slightly different sizes and effects, depending on whether we're drawing it, filling it with anti-aliasing, filling it without anti-aliasing, or making a region out of it-and that makes it much harder to maintain control at the pixel level. In fact, in my opinion, this issue results in a drawing model that is a bit of a mess.

I have written custom controls for all sorts of platforms: for versions of Microsoft Windows from version 2.0, for the Macintosh, for OS/2, for X/Motif (and even OpenLook), and other windowing systems too obscure to mention. Most of these windowing systems define their own semantics for the behavior of drawing operations, and some even change behavior from version to version. In some cases, I wrote some custom controls that were portable between all of the above windowing systems, with the drawing operations abstracted out, so that the bulk of the code was the same from platform to platform.

However, the variety in drawing operation behavior between different platforms was problematic. Out of sheer necessity, to solve this issue I devised an alternative coordinate system model. I wrapped all of the drawing operations on all of the platforms with this alternative coordinate system model, and this allowed me to write many custom controls that were portable between all of the platforms. Along the way, I found that it was much easier to program with my new coordinate system model. I've shared my coordinate system model with many other programmers, with some positive feedback. Now, even if I don't intend to port a custom control to another platform, I still wrap the graphical operations so that I can use this coordinate system-just because it is easier to program.

I call this coordinate system the Outline Model coordinate system. It is implemented in a single class, called the GraphicsOM class. It is easy to use, and is it particularly useful when writing custom controls in which we would like to have precise control over which pixels are drawn. Almost all screens have a resolution of less than 123 DPI, and most have a resolution of around 96 DPI-and with screens of this level of resolution, control of individual pixels is very important to the overall effect we're trying to create.

In this chapter, we will:

  • Demonstrate the issue that we have with the default coordinate system in GDI+

  • Demonstrate and explain the Outline Model coordinate system, how to use it, and how it solves the problems described

  • Take a look at some 3D raised and inset effects that are included in the GraphicsOM class library

The GraphicsOM class is too large to include as a listing in the book, but it is available for download (along with the other support code for this book) at http://www.wrox.com. Feel free to use this class in your custom controls and applications.


Defining the Problem

The problem we have is this: when we define a shape or line of a given pixel size in our code, the actual pixel size of that shape or line on the drawing surface depends on the operation we're applying. As a direct result, it's much harder to control the color of our graphic at the pixel level. Consequently, it's difficult to arrange shapes and outlines in such a way that they meet exactly without overshooting or falling short, and this ultimately means that (at best) our graphics look untidy, and (at worst) 3D effects and other visual effects are spoiled.

Let's take a look at some examples to remind ourselves of the examples we've seen over recent chapters. The following are examples of lines, squares and regions whose definitions are all ostensibly based on a length of 3 pixels. The diagrams on the left are all drawn with a pen of width 1 pixel:

  • The horizontal line is defined as starting at the center of (0, 0) and ending at the center of (3, 0), and by definition is therefore 3 pixels in length. However, the pen-width is exactly one pixel, and so by operation, each of the four pixels between (0, 0) and (3, 0) inclusive is colored black. The result is a line that measures 4 pixels from end to end.

  • The diagonal line reaches from (0, 0) to (3, 3). By a similar argument, it stretches through a full four pixels, not three.

  • The bottom left figure is of the outline of a square of height and width 3 pixels. By a similar argument, each of its sides is represented by a four-pixel block.

Click To expand

Now compare this behavior with the variable behavior we find in the examples on the right:

  • The first is another square of height and width 3 pixels-but this time it's filled (with no antialiasing). According to the FillRectangle request, the top-left corner of the square should be at (0, 0). However, with anti-aliasing disabled, GDI+ is permitted only to draw whole pixels, so this isn't possible. The result is a square of identical size, but which has been offset by a half-pixel upwards and left (because the 'no anti-aliasing' algorithm is such that the pixels that hang over the left edge and above the top edge are filled, but the pixels that hang over the bottom edge and over the right edge are not filled). Thus we get a filled-in square that's exactly 3 pixels in each direction.

  • The second is another filled square of height and width 3 pixels-but this time anti-aliasing is switched on. The size of this square is the same as the one above, and in fact it represents the FillRectangle request in that it tries to put the top-left corner of the square at (0, 0). To achieve this effect, the edge pixels of the square are half-shaded as we described in Chapter 2.

  • Finally, we've clipped the drawing surface to a 3×3 clipping region and then filled the drawing surface. The clipping rectangle has three pixels on a side, and so a total of nine pixels are affected. In fact, it's similar to the square filled without anti-aliasing.

In all of the above operations, we attempted to draw and fill lines and squares that involved lengths of 3 pixels. The code fragments are written with the intention that the resulting lines and squares are based on lines of length 3 pixels and a square of side length 3 pixels.

In the six different operations, we get three different types of behavior. In the three Draw… operations (on the left), the pen width has an effect so that we actually get a line based on the width of four pixels, not three. Two of the remaining three operations affect exactly the same pixels (the square filled without anti-aliasing and the clipping region). The square filled with anti-aliasing produced an apparent half-pixel shift when compared with the non-anti-aliased filled square, and the edge pixels have anti-aliasing artifacts (that is, they are shaded).

In fact, the inconsistency can be broken down into two distinct issues:

  • First, when we request a Draw… operation, GDI+'s interpretation of the operation results in a graphic that is larger than the dimensions we've specified. This is because the pen width is non-zero, and influences the length of lines drawn (as well as the width). Thus, a 3-pixel line drawn with a 1-pixel pen results in a line of length 3+1=4 pixels; and a 10-pixel line drawn with a 5-pixel pen results in a line of length 10+5=15 pixels. It might be preferable to have a system that represents the pen width in the width of the graphic, but not in the length.

  • Second, some of these actions can be described naturally by thinking of gridlines that run through the center of the pixels (that's especially true of Fill… operations with anti-aliasing). Others are more natural when you consider them in terms of whole pixels, described using gridlines that separate the rows and columns of pixels (for example, defining and using a region). Others (such as the Draw… operations) can actually be described either way. It would be preferable if we used a system of gridlines that described all these operations naturally.

Now we've identified the problem, let's describe how we propose to resolve it.


The Outline Model Coordinate System

Let's take a look at an alternative approach that tackles these problems. We execute a two-pronged attack on our drawing ideas. First, we make a subtle change to the way we think of our coordinate system. Specifically, we'll move the gridlines that we use to describe the positions of the pixels. Thus, instead of running the gridlines through the centers of the rows and columns of pixels, we'll have those gridlines running between the pixels:

Click To expand

In this system, the expression (x, y) describes the pixel immediately to the right of the xth vertical gridline and immediately below the yth horizontal gridline. (By contrast, in the default system (x, y) describes the pixel centered on the xth vertical gridline and yth horizontal gridline.)

We'll use this slightly revised coordinate system to describe the positions of individual pixels and sets of pixels that we want to draw to. When we perform our drawing and filling operations using this new coordinate system, we will need GDI+ to interpret our instructions slightly differently. To do that, we'll use a custom class called GraphicsOM, which performs the necessary interpretation between our new coordinate system and the default one, and then uses the standard GDI+ Graphics class to draw the translated instructions to the drawing surface.

Second, our GraphicsOM class's Draw methods will also allow us to draw lines and shapes whose actual dimensions are dependent only on the dimensions of the lines and shapes given-and are independent of the pen width.

The GraphicsOM Class

The GraphicsOM class is a simple custom class that replaces of some of the functionality of GDI+'s Graphics class. The GraphicsOM class consists of a number of methods (such as DrawRectangle, DrawLine, and a few others) that we can use in a very similar way to the similarly named methods of the Graphics class. However, they interpret our drawing operations slightly differently-in that they take account of the change in coordinate system, and they draw lines whose length is independent of pen width.

The GraphicsOM class includes all code for drawing horizontal lines, vertical lines, diagonal lines, and filled rectangles, as well as all methods that draw raised and inset rectangles and lines. Most of these methods don't do a lot; they just alter coordinates, and then call their corresponding method in the Graphics class.

We won't cover the source code for the GraphicsOM class here. It is available at http://www.wrox.com, if you would like to examine it or use it. However, we will explain how to compile the class into a usable class library, and how to employ it in your code.

Building the GraphicsOM DLL

You don't have to build the GraphicsOM class library-if you prefer, you can use the Wrox.GraphicsOM.dll file that is already compiled and ready to use, and included in the source code for this book at http://www.wrox.com.

If you do intend to compile the class library for yourself, then here's how you can do it in Visual Studio .NET:

  • Create a new Visual C# Class Library project and give it a name (perhaps Wrox.GraphicsOutlineModel). Place a copy of the file GraphicsOM.cs (available as part of the source code for this book) in the directory for the new project.

  • In the Solution Explorer window, delete the existing Class1.cs file. Then right-click on the project in the Solution Explorer window, and select Add | Add Existing Item from the context menu. Select the GraphicsOM.cs file in the dialog, and add it to your project.

  • In the Solution Explorer window, right click on References, and select Add Reference from the context menu. Select System.Drawing.dll in the listbox; click the Select button, and then the OK button.

  • Build the DLL by selecting Build | Build Solution on the menu. Now look in the /bin/debug directory of your project-you will see the Wrox.GraphicsOutlineModel.dll file that you've just created by compiling the code.

That's it-you can close this project. Now we're ready to test the GraphicsOM class by using it in some sample code.

Using the GraphicsOM Class

The methods of the GraphicsOM class work in just the same way as their namesake methods in the Graphics class. Using these methods is quite easy.

In order to use the GraphicsOM class in drawing code, we first need to add a reference to the Wrox.GraphicsOutlineModel.dll class library file (in the Solution Explorer window, right-click on the References node and select Add Reference, then click the Browse button, and navigate to the Wrox.GraphicsOutlineModel.dll file and select it).

The class belongs to a namespace called Wrox.GraphicsOutlineModel. We need to add a using statement to indicate that we're using code from that namespace:

using Wrox.GraphicsOutlineModel;

Now we can use the GraphicsOM class in our painting code. We will write a Paint event handler in the normal fashion. Inside the event handler, we declare and instantiate an instance of the GraphicsOM class, passing the Graphics object to the constructor:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    GraphicsOM gOM = new GraphicsOM(g);
    ...
}

Now we can set properties such as clipping and smoothing mode using the Graphics class, but call drawing and filling operations using the GraphicsOM class. The GraphicsOM object will interpret our instructions and draw them correctly on the screen (as we've described above). Let's have a look at some examples.

The Effect of Using the GraphicsOM Class

First, let's state at the outset that the GraphicsOM class uses identical semantics to the Graphics class when it comes to regions and clipping, and hence its behavior does not change. Thus, a region clipped using the GraphicsOM class looks just like one that is clipped using the Graphics class:

Click To expand

Now let's revisit the DrawLine and DrawRectangle examples we saw earlier. The following three examples now use DrawLine and DrawRectangle method calls that specify the same dimensions as before, but now the operations are requested through the GraphicsOM class, instead of the Graphics class. As a result, the resulting lines and shapes more accurately reflect the lengths that we intended:

  • When drawing a horizontal line with a pixel width of one, we define that the pixels drawn fall just below our grid line. In the example below, the line drawn from point (0, 0) to point (3, 0), draws three pixels just below the 0th horizontal gridline. In a similar fashion, a vertical line drawn, say, from (10, 10) to (10, 15) will consist of five (not six) pixels immediately to the right of the 10th vertical gridline.

  • When drawing a diagonal line, we use a rectangle whose diagonally-opposite corners are the beginning and ending points of the line, The two pixels just inside these corners of the rectangle are affected by the drawing operation, and a straight (or as straight as possible) line is drawn between those two pixels. The example following shows a diagonal line drawn from pixel (0, 0) to pixel (3, 3)-the line is contained between the 0th and 3rd gridlines in each direction.

  • When drawing a rectangle with any pixel width, the pixels drawn will, by the definition of GraphicsOM, fall just inside of the gridlines that make up our rectangle. For example, if we use a 1-pixel pen to draw a rectangle whose upper-left corner is at (1, 1), and whose width and height are defined to be 3 pixels, GraphicsOM draws the rectangle shown at the bottom of this figure.

Thus, in each of these cases, if we were to set the clipping region of our drawing surface to a given rectangle, and then use the same dimensions to draw a rectangle or diagonal line with GraphicsOM.DrawRectangle or GraphicsOM.DrawLine, the drawn line or rectangle would fall just inside the clipping rectangle. This is one advantage that GraphicsOM offers us over the Graphics class-that the Draw… operations are more closely related to the region operations.

Click To expand

Now let's look at the Fill… operations, with and without anti-aliasing. In fact, when we use GraphicsOM.FillRectangle to fill a rectangle with integer dimensions, it doesn't matter whether we use anti-aliasing or not. The result is the same. For example, the following figure shows what happens when we fill a rectangle whose top-left corner is at (0, 0) and whose width and height are both defined to be 3 pixels. The one on the right is anti-aliased:

Click To expand

Both of these are identical to the result we got when we Filled the same rectangle using Graphics.FillRectangle with anti-aliasing turned off.

However, this doesn't mean that GraphicsOM disables anti-aliasing altogether! Take a look at the following two examples. They show the results when we draw a 3.5 pixel square (using a RectangleF), with anti-aliasing switched on. On the left, we've used Graphics.DrawRectangle. On the right, we've used GraphicsOM.DrawRectangle:

Click To expand

In each case, this is what we would expect. The anti-aliasing effect takes care of the fact that the square has non-integer dimensions. In both examples, the point (0, 0) is the top-left corner of the rectangle. In the coordinate system on the left, GDI+ represents this by anti-aliasing the (0, 0) pixel to a 25% gray shade. In the coordinate system on the right, the (0, 0) pixel is filled in black-and the right-most and lowest pixels have been anti-aliased.

More importantly, the system on the right is more compatible with the clipping region definition system.

Design Patterns and the Graphics Outline Model

Before we move on, I'd like to say a little more about the motivation and thinking behind the GraphicsOM class.

As I've said, my motivation for setting up the GraphicsOM class was two-fold: that Draw…operations tended to use the length of the pen to determine the final length of a line or shape, and that the Fill… operations work differently from region operations for similar shapes.

Ideally, this solution would be implemented within the Graphics class itself. We would specify our choice of coordinate system by setting the PageUnit property of the Graphics class. Unfortunately that's not the case.

If it had been possible, I would have implemented this model by deriving from the Graphics class. Unfortunately this is not possible, because the Graphics class is sealed.

Instead, I've used a variation of a design pattern, known as the decorator pattern, to create a class with this functionality. In case you didn't know, design patterns are based on the idea that experienced programmers repeatedly come across similar types of problems-and that similar types of problems can be solved using similar solutions. This is also true in object-oriented design and programming-programmers come across different problems that have similar characteristics, and find that they can apply a single solution to every one of those similar problems. The subject of design patterns is about identifying and naming these patterns. As a result, it standardizes the approach that we take to solve a given type of problem; it gives us a better chance of applying the optimal solution to a problem; and it provides a lexicon to programmers so that they can communicate more rapidly about the design of class hierarchies.

If you would like to know more about the concept of design patterns, you could try Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, ISBN 0-201-63361-2).

The decorator pattern allows us to add functionality to a class without sub-classing it. The idea here is that we make a set of methods with the same signature as the methods that we want to modify the behavior of. When we instantiate the GraphicsOM class, we pass an instance of the Graphics class to the constructor, which gets held in a private member variable of the GraphicsOM object. When we call a method to perform a graphical operation, it collects the values passed in as parameters and modifies them to express our desired behavior in terms of the system used by the Graphics class-and then calls the corresponding method in the Graphics class to do the job.

Pros and Cons of GraphicsOM

There are a number of advantages that the GraphicsOM coordinate system has over the default one:

  • When making a complicated custom control, it is convenient to divide the control up into rectangles, and write methods that are responsible for drawing all the pixels within a rectangle-but are not responsible for (and should not affect) any pixels outside of that rectangle.

  • We can pass integer coordinates to our methods, but we don't need to worry about whether anti-aliasing is turned on or off.

  • There is a simple, direct relationship between the gridlines and the number of pixels affected by any drawing operation. For example, if we draw a rectangle whose left edge is at X=25, and whose right edge is at X=60, then the resulting rectangle will be 60−25=35 pixels wide.

  • Computation of clipping rectangles is made significantly easier, as they are expressed in exactly the same terms as our drawing operations. As we've mentioned, clipping doesn't work particularly well with the default GDI+ coordinate system.

Disadvantages of this model:

  • It is non-standard. Maintenance programmers will need to be trained in this alternative coordinate system.

  • It may cause conflicts if it is used by some members of a programming group and not others. It may be desirable to standardize within a group or project, so that all developers use it, or none do.

The GraphicsOM coordinate system needs to be used only when we are drawing a number of lines and rectangles that need to provide very precise coverage over a certain area of the form or window. It may not be appropriate if we are doing drawing where we will use anti-aliasing. We don't need to use it when drawing text.

There is no significant performance penalty associated with using the GraphicsOM class. The relative cost of creating and using the GraphicsOM class to the costs of implementing the actual graphical operations and writing to the video card or display device are minuscule.

Personally, I have found this approach to be a convenient and usable programming model that wears well with time. It yields maintainable programs.


Creating a 3D Effect

Since this chapter relates to the subject of drawing with exacting pixel precision, it seems reasonable to include a discussion of how we can create portions of our custom control that have a 3-dimensional (raised or inset) appearance. It's common to see such effects in custom controls, and it they are not difficult to create.

The GraphicsOM class includes a number of raised and inset effects. The raised effects are called Ridge and Raised, and the inset effects are called Groove and Inset. There are two depths for each style. To illustrate, they're all shown below:

Click To expand
Click To expand

As you can see, Ridge and Groove are visually inverse to one another, and Raised and Inset are also visually inverse to one another. In each case, we use light and dark colors to create the 3D effect-the lighter colored areas give the effect that strong light is falling on an angled surface, while a darker shade gives the appearance of a surface that is angled away from the light and therefore in shadow. The 'heavily-lit surfaces' and 'shadowy surfaces' combine to give the desired 3D effect-if we get the shades and lines right!

Note that in these effects, the light source always appears to be at the upper left. We've used this because Microsoft Windows uses the same position for its light source, and because our controls will be used in the Windows environment. If we'd placed our light source elsewhere (say, at the top-right), then our controls would look very strange when viewed in Windows, where the light-source for every other control is at the top-left.

Ideally, we would implement this functionality using custom pens and brushes. However, this is not feasible using the .NET Framework. GDI+ was designed in such a way that we can't derive from the Pen and Brush classes to add our own functionality. Instead, we can add this functionality to the GraphicsOM class, which will serve us almost as well.

Using System Colors

When drawing using 3D effects, it's best to use the colors that are used elsewhere in the system for similar controls. Thus, if the user decides to change their system color scheme, our control will reflect the scheme. We can get these colors from the System.Drawing.SystemColors class. There are five properties in this class that are specifically intended for us to use in order to confer a 3D appearance in our graphics:

Property | Usage
Control | The face colour
ControlLight | The light color of a 3D element
ControlLightLight | The highlight color of a 3D element
ControlDark | The shadow color of a 3D element
ControlDarkDark | The darkest color of a 3D element

Here's how we use these five colors in the Raised 3D graphic:

Click To expand

Drawing 3D Lines and Rectangles

The GraphicsOM class includes two overloaded methods-Draw3DLine and Draw3DRectangle-that create these 3-dimensional effects. The way we use these methods is similar (though not identical) to the way we use the Graphics class's DrawLine and DrawRectangle methods-they differ in that we don't need to pass a Pen object to these functions, but we do need to specify whether our line or rectangle should have a Groove or Ridge effect.

Because these methods are part of the GraphicsOM class, their behavior is consistent with the other drawing methods in the class:

  • When we draw a horizontal 3D line, it runs directly below the specified gridline

  • When we draw a vertical 3D line, it runs directly to the right of the specified gridline

  • When we draw a 3D rectangle, regardless of its style, and regardless of its depth, the rectangle is drawn just inside the gridlines that define its boundaries.

GraphicsOM 3D Source Code

We'll cover a little of the structure here, but not all of the code. You're welcome to examine the source code for the GraphicsOM class, which is available as part of the download for this book.

The ThreeDStyle Enumeration

In the GraphicsOM source, there is an enumeration (called ThreeDStyle) that allows us to specify the style of our lines and rectangles:

public enum ThreeDStyle
{
   Groove,
   Inset,
   Raised,
   Ridge,
}
The Draw3DLine and Draw3DRectangle Methods

There are two overloads for the Draw3DLine method:

public void Draw3DLine(p1, p2, style, depth);
public void Draw3DLine(x1, y1, x2 , y2 , style, depth);

Here, p1 and p2 are of type Point; x1, y1, x2, and y2 are integers; style is one of the ThreeDStyle enumeration values; and depth is 1 or 2.

Here's an example:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    GraphicsOM gOM = new GraphicsOM(g);

    gOM.Draw3DLine(0, 10, 500, 10, ThreeDStyle.Ridge, 1);
    gOM.Draw3DLine(0, 20, 500, 20, ThreeDStyle.Groove, 1);
    gOM.Draw3DLine(0, 30, 500, 30, ThreeDStyle.Ridge, 2);
    gOM.Draw3DLine(0, 40, 500, 40, ThreeDStyle.Groove, 2);
    gOM.Draw3DLine(0, 50, 500, 50, ThreeDStyle.Raised, 1);
    gOM.Draw3DLine(0, 60, 500, 60, ThreeDStyle.Inset, 1);
    gOM.Draw3DLine(0, 70, 500, 70, ThreeDStyle.Raised, 2);
    gOM.Draw3DLine(0, 80, 500, 80, ThreeDStyle.Inset, 2);
}

This code produces a number of lines that give various horizontal 3D effects:

Click To expand

There are also two overloads for the Draw3DRectangle method:

public void Draw3DRectangle(r, style, depth);
public void Draw3DRectangle(left, top, width, height, style, depth);

Here, r is a Rectangle; left, top, width, and height are integers that represent the position of a rectangle; as before, style is one of the ThreeDStyle enumeration values; and depth is 1 or 2.

Here's an example of this:

private void Form1_Paint(object sender,
    System.Windows.Forms.PaintEventArgs e)
{
    Graphics g = e.Graphics;
    GraphicsOM gOM = new GraphicsOM(g);

    gOM.Draw3DRectangle(10, 10, 50, 20, ThreeDStyle.Ridge, 1);
    gOM.Draw3DRectangle(80, 10, 50, 20, ThreeDStyle.Groove, 1);
    gOM.Draw3DRectangle(10, 50, 50, 20, ThreeDStyle.Ridge, 2);
    gOM.Draw3DRectangle(80, 50, 50, 20, ThreeDStyle.Groove, 2);
    gOM.Draw3DRectangle(10, 90, 50, 20, ThreeDStyle.Raised, 1);
    gOM.Draw3DRectangle(80, 90, 50, 20, ThreeDStyle.Inset, 1);
    gOM.Draw3DRectangle(10, 130, 50, 20, ThreeDStyle.Raised, 2);
    gOM.Draw3DRectangle(80, 130, 50, 20, ThreeDStyle.Inset, 2);
}

This code produces a number of rectangles, which should give you an idea of how they work:

Click To expand


Summary

In this chapter, we discussed the various ways in which coordinate systems that are used with drawing and clipping don't really work together in a natural way. We showed examples of several drawing operations using GDI+-specifically, Draw… and Fill… operations and operations involving regions. We noted how, even though the operations have similar coordinates, the parts of the drawing surface affected by these operations are rather different at the pixel level. When we're drawing to drawing surfaces that have a resolution of around 96DPI-as most screens do-this level of detail is sufficient to spoil our 3D and other effects, or simply make our controls look untidy.

We took a look at the GraphicsOM class, which implements an alternative coordinate system. One of the advantages of this coordinate system is that if we pass identical coordinates to methods such as drawing rectangles, filling rectangles, clipping, invalidation, and drawing lines, an identical set of pixels is affected in each case.

Finally, we looked at how the GraphicsOM class uses these techniques to draw rectangles and lines that have a raised or inset appearance, and how these operations have nearly identical behavior to the other GraphicsOM drawing methods. We didn't discuss the source code of the GraphicsOM class here, but it is available as part of the source code that accompanies this book-which you can download from http://www.wrox.com.


Chapter 11: Architecture and Design of Windows Forms Custom Controls

Overview

In this chapter, we will introduce the concept of the custom control-the real aim of this book. If you are reading this, you won't need to be reminded of the importance of being able to create custom controls that present to users user interface features that are not directly available from the pre-defined Windows Forms controls supplied by Microsoft.

We will start off by reviewing some of the characteristics of custom controls and components in some detail. Then, we will move on to the practical side and start developing some custom controls. Having built a basic control, we will go on to develop more sophisticated controls that illustrate various aspects of usability and how to implement controls that behave in accordance with user expectations. There are several aspects of custom controls we will consider:

  • Properties- We will add properties to a control and show how to inform Visual Studio .NET of default property values, so that the IDE can display properties in a way that makes it easy for other developers to reuse your control. This area, incidentally, touches on design-time properties, a topic that we will explore more fully in the next chapter.

  • Focus and Focus Cues- This is a more complex topic than you might think. We will look at how controls should behave when they participate in the tabbing order of a form and receive or lose the focus. As part of this, we look at how to correctly display keyboard UI cues.

  • Events- Events occur everywhere in Windows Forms, and it is likely that at some point, you will need to define your own events instead of using the Microsoft-defined ones. We will briefly review event and delegate architecture, and then develop a sample that shows how to do this.

  • Deriving from Existing Controls- Another very valuable technique is to derive our custom control from an existing Windows Forms control. This allows us to easily add custom functionality to standard controls. We will see how to do this.

  • User Controls- If we build a custom control out of several constituent controls, we can build some complex functionality that we can use over and over in our applications. Any situation where we find ourselves using the same pattern of controls is a candidate for this technique.

Once we have done all that, we will finish off by looking in a bit more detail at some of the more theoretical considerations of control design, which you might want to consider when you are working on your own commercial controls.


Basic Principles

Most developers who are reading this book will already have a good understanding of both components and custom controls. They have been around in one form or another for quite a while. However, the .NET Framework adds some additional characteristics to them that are worth discussing.

Since a custom control is a special type of component, we will first define exactly what a component is, then cover the specific case of custom controls. Components and controls as implemented in the .NET Framework have some great enhancements over previous implementations of them.

Component technology is very similar to object-oriented technology. In the .NET Framework, the two are closer than ever. However, there are aspects of component technology that are not part of a traditional OO technology. We will detail these aspects in this section.

First, let's discuss the specific mechanisms whereby .NET implements components and custom controls.

Whether we are building a Windows Forms rich-client application, or an ASP.NET web application, the techniques of building components are identical. However, the techniques for building custom controls are quite a bit different. This chapter focuses on building custom controls for Windows Forms. In Chapters 17, we will explore the Web side of the world.

The advantages of components and custom controls are:

  • Components are commonly used as a means to conceptually break an application into smaller pieces.

  • They make our software systems more modular.

  • They enable greater levels of code reuse.

  • They can be a convenient way to divide a large application between multiple programming groups. A programming group can be responsible for delivering a specific component, or perhaps a set of components.

  • Buying ready-made components and custom controls allows developers to use their time better in creating the custom aspects of their application without worrying about more generic issues.

In addition, there is another use for components and custom controls, especially in .NET, as .NET components have a very good mechanism for implementing properties. We can use components and controls to implement a declarative style of programming. This is a particular programming style and philosophy that increases programmer productivity, increases the robustness of the system, and decreases maintenance time. We will look into this programming style later.

When we are building components and custom controls, there are a couple of decisions that we need to make. We need to decide what components and custom controls to build, and we need to decide what the programming interface to those components is to be. The analysis that we perform when looking at our design with respect to declarative programming can lead us to good design of our components and custom controls.

What is a Component?

In this section, we want to define conceptually what a component is. Components are an implementation of certain abstract ideas, and it is helpful to understand these ideas. Understanding the intent helps us design better components.

There is a fair amount of debate in the software industry as to exactly what a component is, and what it isn't. Some technologies don't include all of the characteristics listed below, and proponents of those technologies argue that the characteristic should not be a required part of component technology. Presented here is one definition, certainly influenced by the .NET implementation of components. I think that all of the characteristics listed below are justified, and in those cases where it might be debatable, I will provide my arguments as to why the particular characteristic is justified.

Some aspects of components overlap aspects of object-oriented programming, but there are differences. We will summarize the differences after we detail the characteristics of a component.

Some characteristics of components are:

  • Encapsulation -A component encapsulates an abstraction or set of related functionality. In the same manner as OOP, a component packages data and procedures within it, hiding the implementation.

  • Interface -A component has a clear and specific programming interface. Sometimes this is called a contractually-specified interface. Object-oriented developers working with a large class hierarchy often find themselves changing classes quite often. The ease with which we can do this and not break existing code is one of the strengths of OOP. In contrast, components implement a specific interface, and if the contract with the users of the component changes, we define a new interface. In other words, we don't change the external interface of a component so readily.

    A component only allows access to its data and behavior through its programming interface. What we are referring to here is the programming interface. We are not referring to the user interface, which is a characteristic of a control.

    The interface to a component is made up of properties, methods, and events. There are some technologies that don't have all three of these, but I consider all three to be vital:

    • Properties- Properties enable the user of the component to configure it in a declarative fashion. A declarative style of programming has such an advantage over the alternative that in many developer's minds, the idea of using a component without being able to bring up a Properties window to configure it would not even occur to them. Further on in this chapter, we will discuss the generalization of declarative programming. It is a very valuable way to analyze and develop large software systems.

    • Methods- All component technologies have methods.

    • Events- In today's event-driven world, components need to be able to both implement event handlers to receive events, and generate events to be sent to the module using the component.

  • Inheritance- Most component technologies before .NET did not include inheritance as a feature. However, it is very valuable to be able to declare a class that has a component as its base class. This allows us to do such interesting things as deriving from an existing control, and modifying some part of its behavior while inheriting the remainder of its behavior.

  • Introspection- This is a mechanism whereby users of the component can programmatically discover what the interface to the component is. This facilitates an Integrated Development Environment (IDE) such as Visual Studio .NET to present the component to the developer so they can use it in a manner that is easy to manipulate.

    Introspection is also used so that a program can discover new components and integrate the behavior of the components in the program. An example of this might be a digital photograph manipulation program that allows for custom add-ins. However, the vast majority of the use of introspection is for integration into an IDE.

  • State- When we instantiate a component, it keeps state for our instance, and we can manipulate the state through the interface. We can implement stateless components, and often it is desirable to do so, but a good component technology allows components with and without state.

  • Persistence- When a component is used in an IDE, and the user has configured it in a certain way, there needs to be a mechanism to save the state of the component. In some technologies, this is implemented as object serialization. In other technologies, there are automated procedural code parsing and generation technologies.

  • Pluggability- Separation of the interface from the implementation allows us to plug in one of a number of components at run-time to achieve desired functionality. An example of the use of pluggability is the implementation of database independence. We can define a component interface, and so long as we use a component that meets the interface definition, we can plug a variety of database engines into our application.

  • IDE compatibility- While perhaps not part of the theoretical definition of a component, all successful implementations of component technology can be used in an IDE. Developers can drag-and-drop components into their application, configure their component, and often see a design-time representation of their component.

  • Packaging and Distribution- Components often are made up of classes, icons, bitmaps, and so on. A component technology must have a comprehensive infrastructure for packaging a completed component, and distributing it to other developers who will write code to use the component, as well as end users, who use applications utilizing the component.

    One of the problems that Microsoft has solved with .NET is that of deploying and using multiple versions of the same component on a given machine.

  • Licensing- For a component technology to be successful, there needs to be some mechanism for licensing. Sometimes, developers of a component want to license their component to other developers, but then allow the end users of an application to use the component freely. Licensing is a technology to control intellectual property.

  • Distributability- One of the major advantages of using components is that we can decide whether a component will run in-process, in a separate process on the same machine, or on a remote machine. The ease with which we can change the distribution of components is a key element of building scalable solutions. Distributed computing is one of the fundamental advantages to building applications in a managed environment such as .NET.

    When we are building a distributed system, we will want to write code that creates remote objects. Marshaling is the process of sending objects across boundaries.

    We can build a remotable component that is marshaled either by reference or by value. When an object is marshaled by reference, a proxy is created that accesses the object using remote calls. When an object is marshaled by value, a serialized copy of the object is sent to the destination.

  • Security- There needs to be some method to verify that components have the right to do what they are doing. This is only possible in a managed environment with a runtime.

Important

In the .NET Framework, any component that we build must implement the System.ComponentModel.IComponent interface, or be derived from a class that implements it. Generally, we will not implement the IComponent interface ourselves, but will instead derive directly or indirectly from the Component class. Note that System.windows.Forms.Form does derive indirectly from Component.

In previous implementations of component technology, there were clearly parts of object-oriented programming (OOP) such as inheritance, that were not part of the component technology, and there were parts of component technology such as distributability that were not part of OOP. However, component technology in .NET now has all of the features of OOP. The following features of component technology are not part of pure OOP.

  • Pluggability- In .NET, the mechanism by which we get pluggability is through components.

  • IDE Support- In .NET, IDE support is different for components and classes. Components have interactions with the design window that pure classes do not.

  • Packaging and Distribution- The way that we package and distribute a class is to make a component from it.

  • Licensing- We can't control licensing of a class as we can with a component.

  • Distributability- Without making a component from a class, we can't make the decision as to whether the class will run in-process, out-of-process, or on a remote machine.

  • Security- If we want to control certain aspects of security, we need to use components. The security system of the Common Language Runtime (CLR) applies Code Access Security permissions at the granularity of an assembly.

The question might be asked, if components encompass all of the benefits of OOP, why wouldn't we always build components? Component technology adds quite a bit of overhead, both in design, and in implementation. If we don't need the benefits of component technology, it is not a good idea to use it.

What is a Custom Control?

Custom controls are components that have a visual aspect to them, will be part of the user interface, and will be configured in the design window. In effect, custom controls are components with additional responsibilities. All custom controls are components, but the reverse is not true.

There are three basic types of custom controls:

  • A custom control built from scratch. In practice, this normally means deriving the control from System.Windows.Forms.Control.

  • A custom control built by deriving from an existing Windows Forms control; for example, you might derive a specialized TreeView from System.Windows.Forms.TreeView.

  • A custom control built as a composite of two or more Windows Forms controls. Such a control is in .NET termed a user control, and is normally derived from System.Windows.Forms.UserControl.

Building a custom control from scratch is the most flexible approach, but requires the most work. When we build a control from scratch, we are responsible for taking care of the drawing of the control. In addition, we will need to handle certain events, such as mouse and keyboard events. We may also want to send events to the user of our custom control.

When we create a custom control by deriving from an existing control, we don't normally need to take care of any drawing-the base class handles that. The base class will also handle most events, including keyboard and mouse events. The base class for this type of custom control is the existing control class. We would take this approach when there is an existing Windows Forms control that contains most of the functionality that we want. In our custom control, we can handle events from the base class, add additional functionality that is perhaps driven by properties that we have defined in our custom control, and raise new events for the user of our control.

For composite controls (user controls), we build a custom control out of two or more existing controls, called constituent controls. We can position the constituent controls relative to each other. Then, when we place our composite control on a form, the constituent controls are placed relative to the position of the composite control. We can handle any of the events of the constituent controls, implement additional functionality that is perhaps driven by properties that we added to our custom control, and raise events.

With this type of custom control, we can build higher-level abstractions. This approach has two advantages:

  • Higher-level abstractions reduce the work that the developer using the control needs to do

  • By building composite controls that will be used throughout an application or a set of applications, we can enforce a level of consistency

As noted above, this type of control derives from System.Windows.Forms.UserControl, and contains its constituent controls as member fields.


Building a Windows Forms Custom Control

In this section, we will give an overview of the process of building a custom control and we will actually build a very simple control. Here, we are simply trying to provide a conceptual framework of the steps involved:

  • Generate a DLL, not an EXE-When we start a new custom control project, the first thing that we need to do is to create a project where the Output Type is a Class Library, not a Windows Application. When we compile, the compiler will generate a DLL, not an EXE. The normal way to do this is to specify the use of the Windows Control Library template in the New Project dialog box.

    Once you have used the New Project dialog box to build a new project using a template, the next thing you need to do is to change the base class, if you need to. By default, the Windows Control Library template sets the base class to UserControl. The base class determines the type of control that we are going to build.

  • Write properties-We need to code up any public properties. Properties are an important part of the programming interface to components and controls. Appropriate thought should be given to their design.

  • Code up methods-Write any public (and private) methods.

  • Code up event handlers-Handle any events of the base class. If we are building a composite custom control, we need to handle any necessary events of the constituent controls. Normally, we can use Visual Studio .NET to create the empty event handler, and wire up the event handler to the event.

  • Declare and raise events-If we need to raise any events, we can do so. We can either use existing events, or create new events with their own arguments.

  • Implement design-time features-While this step isn't necessary to make a simple custom control, for more elaborate custom controls, this step will make them much easier to use.

  • Customize the Toolbox window-This step puts our custom control in the Toolbox window so that we can drag and drop it onto forms. The control needs to be compiled before we can take this step.

One important note is that if we want our component or custom control to be usable from other .NET programming languages, we must ensure that all public and protected members of the control are Common Language Specification (CLS)-compliant. The C# language is a CLS-compliant language, but there are features in C# that are not CLS-compliant. If we use those features in the external interface to our control, other developers may not be able to use the control from a different language.

The Simple Custom Control Sample

In this exercise, we will build a very simple custom control. All this control does is to paint itself yellow, and draw Hello, World on itself. By creating it, we will be able to examine in detail the steps needed to create a custom control.

First of all, start Visual Studio .NET and on the menu, select File | New | Project…. We will need to create a new C# Windows Control Library, so select the relevant options, name the project SimpleCustomControl, and click the OK button. This will give us a project that will build to a DLL, and some boilerplate code that defines a class for the control. However, we suggest you don't build the code immediately. There are some changes we need to make to the IDE-generated code first.

We also suggest that in the New Project dialog, you click the More button to display advanced options, then check the Create directory for Solution checkbox. This is because we will ultimately be adding another project to the solution to test the control, and so, it is neater if the solution has a proper directory structure.

First, in the Solution Explorer, right-click on UserControl1.cs and rename it to SimpleCustomControl.cs. Next, we need to change the base class for the control. The default base class, UserControl, is, as we mentioned earlier, specifically intended for controls that act as containers for other standard controls. We will see an example of this later on in this chapter, but in most cases, you will be building custom controls-that is controls that are derived from System.Windows.Forms.Control or some other control class. That will be the case in this example, so change the base class to the System.Windows.Forms.Control class:

public class SimpleCustomControl : System.Windows.Forms.Control
{

Now, prior to editing the source code, and changing the base class to the Control class, the Design window looked something like this:

Click To expand

However, after changing the base class to the Control class, the design window looks something like this:

Click To expand

We no longer have a grid onto which we can drag other controls. Visual Studio .NET only supplies the grid if the control is derived from UserControl, for the obvious reason that UserControl is the class designed for laying out other contained controls. However, even though we don't have a grid, the design window is still useful since it allows us access to the Properties window to change events and properties of the control.

Now, using the Properties window, rename the control itself (that is, the (Name) property of the class) to SimpleCustomControl. Obviously, in your own projects, you will choose whatever name is appropriate for your control. Alternatively, you can change the class name by editing the code directly, but it is easier to use the Properties window since that allows Visual Studio .NET to change all occurrences of the class name, for example in the class declaration and in constructors.

Next, we want to add a Paint event. If we open the Properties window by pressing F4, then click on the Events button (the yellow lightning bolt) in the toolbar of the Properties window, we will see the events of the base class for which we can write event handlers. Using the Events pane of the Properties window saves us the effort of writing the empty event handler and wiring the event handler to the event.

Sometimes, even though we can see the design window, it doesn't really have the focus, and it is necessary to click on it to give it the focus, so that the Properties window will be populated with the custom controls information.

Double-click on the Paint event to create an event handler for it, and modify it as follows:

private void SimpleCustomControl_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.FillRectangle(Brushes.Yellow, ClientRectangle);
  g.DrawString("Hello, world", Font, Brushes.Black, 0, 0);
}

The ClientRectangle reference in the call to the FillRectangle() method is a property of the Control class, so it returns the client rectangle of our custom control, not the ClientRectangle of the form, which contains our custom control.

The control is now ready to build. However if you try to run it, you will just get an error message complaining that you can't run a DLL because it doesn't have an entry point. To run it, you will need to create an EXE project to host the control; that is the subject of the next section.

Using a Custom Control

A custom control is not a complete application. We can't run it in a standalone fashion, but we can write a small test application to test our custom control.

The general procedure to test a custom control is as follows:

  1. Compile the control first, because the following step requires it. One method is to use the keyboard shortcut, which is Ctrl+Shift+B.

  2. Create a Windows Forms project in the same solution, and set this project to be the startup project. Then, insert your control onto the new form and write whatever testing code is appropriate in the new project.

Allowing multiple projects within one solution is convenient. It allows us to develop our custom control and our program that tests the custom control in the same instance of Visual Studio .NET. If we modify our custom control or modify our test application, Visual Studio .NET will automatically compile everything that needs to be compiled before running the application. If we were to put our custom control in a different solution, we would need to manually switch to another instance of Visual Studio .NET, compile the custom control, switch back to our test application, compile it, and then run it. While developing, it is much better to put the two projects into a single solution. Later, after our custom control is fully developed, we can use it in applications without adding the entire project to the solution-we can simply add a reference to it.

Customizing the Toolbox

One nice feature of Visual Studio .NET is that if you are working on other projects in the same solution as the control, then the IDE will, in most cases, automatically place your newly built control in the Toolbox, allowing you to drag it onto the form in the same way as you would for say a Button or a TextBox or any of the standard Windows Forms controls. The screenshot below shows our SimpleCustomControl available for use from the Toolbox:

Click To expand

However, you may find that the control isn't there. This will probably be the case if you are using the custom control in a production project rather than simply testing it. That is because for a production project, the control project will probably not be part of the same solution as the Windows Forms project. You may also have problems if you built the control before renaming it, because Visual Studio .NET may have put the control in the Toolbox using its old name, and will then be unable to locate it after the name change (that's the reason why we earlier told you not to build the control straight away). If, for any reason, the control is not already in the Toolbox when you want to add it to another project, then you can get it there by customizing the Toolbox.

To do this, open the Toolbox, if it is not already open (an easy way to do this is to use the shortcut Ctrl+Alt+X). You must have the Design window open to see the Windows Form tab in the Toolbox. In the Solution Explorer, right-click on the control-in our case, SimpleCustomControl, and pick View Designer in the context menu. In the Toolbox, right-click on the Windows Forms tab, and select the Customize Toolbox… dialog. Click on the .NET Framework Components tab, then click on the Browse button, and navigate to the bin\debug sub-directories in the folder that contains SimpleCustomContol. We can then see SimpleCustomControl.dll, which we built when we compiled our custom control.

Obviously, if you are using a completed and debugged control in a production environment, then you should navigate to wherever the Release version of the control is, not the Debug version!

Double-click on it to add it to the Toolbox. Notice the checkbox next to the control we just added. By default, this is checked, but if you wanted, you can remove it from the Toolbox by unchecking it:

Click To expand

Click on OK to close this dialog box. The control will now be in the toolbox.

One final point-you may find that if your control is built in a different solution, you will need to add a reference to the assembly that defines the custom control in the containing application. This is done by right-clicking on the project in the Solution Explorer and selecting Add Reference…to bring up the Add Reference dialog.

Creating a Test Application

Now we will put the theoretical run-through of the last section into practice! First of all, select File | Add Project | New Project… and create a new C# Windows Application, naming it TestSimpleCustomControl, and click OK. In the Solution Explorer window, right-click on the TestSimpleCustomControl project and select Set as Startup Project. The Solution Explorer will now display the project name in bold, indicating that it is the startup project.

If necessary, at this stage, make sure the control is in the Toolbox and the new Windows Forms project contains a reference to it.

Now, in the Toolbox window, under the Windows Forms tab, scroll down until you see the SimpleCustomControl entry. Double-click on it, and we will see it placed on our form. If you want, you can move it around on the form, or change its size.

We can run the application now, and we will see our custom control in the window. It doesn't do much, but there it is. This custom control is about as simple as we can make it:

Click To expand

We will not need this custom control in our Toolbox in the future, so let's remove it. Open the Toolbox window if it is not already open. Click on the Windows Forms tab, and scroll until the SimpleCustomControl entry is visible. Right-click on the entry, and select Delete on the context menu.

Components and Custom Controls

One point worth noting is that in Visual Studio .NET, there is a difference between how you use components and how you use custom controls.

When we use a custom control, we drag it from the Toolbox window onto a grid in the design window, which we can open for forms and composite custom controls. We will then see a visual representation of our custom control on the grid. We can click on the instance of the custom control and bring up the properties window for it.

In contrast, when we drag a component from the Toolbox window onto a form, the component is visually added to the Component Designer, which is a special area reserved for non-visible components below the grid in the design window. We can click on the component to alter its properties.

Following is a screen shot of a design window where we have added the DirectoryEntry component to a form:

Click To expand

The Component Designer is the area below the grid.

Working with components is otherwise identical.

Deploying a Windows Forms Custom Control

To deploy a custom control we have created, we should:

  • Build a release version-By default, when we initially create a project for a custom control, we compile it with debug information in the output file. Before deploying a custom control, we should build a release version.

  • Implement licensing if required-If building a commercial custom control, this step may be necessary. Details of licensing are available in the documentation but are beyond the scope of this book.

At this point we have now seen all the steps necessary to build and test a basic custom control. For the next part of the chapter, we will develop more sophisticated controls in order to illustrate some of the principles of coding them up.


Adding Properties to our Custom Control

C# and other .NET languages support properties as first class citizens of languages. There has been some debate as to whether properties should be supported as such. Properties in Java and some other languages are implemented as methods. However, having properties supported as a fundamental part of the language has two main benefits:

  • Properties make it easy for reflection to return just the properties of a class. We don't rely on special naming techniques (sometimes called a magic naming convention). Just as it is bad design to have ‘magic' values for variables, I consider magic naming conventions to be bad design. In addition, interactive design environments can more easily allow the programmer using the control to edit its properties.

  • When writing code, we can get and set properties as though we were setting and getting member variables of a class. This syntax more accurately reflects the nature of properties.

Custom Control with Properties Example

In this section, we will create a custom control that implements a number of properties that determine how the control will display. The control, when running, looks something like this:

Click To expand

The control is simply a panel that displays some text a certain number of times either vertically or horizontally. The properties we will add to it will be:

Property | Description
string displayText | The text to be displayed
int displayCount | The number of times this text will be displayed
Color textColor | The color the text will be displayed in
Font textFont | The font used to display the text
TextDirection textDirection | Whether the text will be displayed horizontally or vertically
Point startingDisplayPoint | The coordinates within the control for where the first line of text will start.

To create the project, create a new C# Windows Control Library project and call it CustomControlWithProperties. In the Solution Explorer, right-click on UserControl1.cs and rename it to CustomControlWithProperties.cs. Also, as before, change the base class using the code editor to the System.Windows.Forms.Control class, and then use the Properties window to rename the class, this time to CustomControlWithProperties.

First, we need an enumeration to describe the text direction, so add this enumeration to the CustomControlWithProperties class:

public enum TextDirection
{
  VerticalText,
  HorizontalText
};

Now add the following private member variables to the CustomControlWithProperties class. These fields will contain the internal values of the properties we have just listed:

private string displayText;
private int displayCount;
private Color textColor;
private Font textFont;
private TextDirection textDirection;
private Point startingDisplayPoint;

Let's look at the implementation of the properties. They will be implemented to simply wrap the above fields, but with the extra feature that when any of these properties is set, we will need to get the control repainted to show its updated appearance. We can do this by calling the Control.Invalidate() method from within the set property accessor. For the DisplayText property, this means the code will look like this.

public string DisplayText
{
  get
  {
    return displayText;
  }
  set
  {
    displayText = value;
    Invalidate();
  }
}

The remaining properties are implemented in a similar manner:

public int DisplayCount
{
  get
  {
    return displayCount;
  }
  set
  {
    displayCount = value;
    Invalidate();
  }
}

public Color TextColor
{
  get
  {
    return textColor;
  }
  set
  {
    textColor = value;
    Invalidate();
  }
}

public Font TextFont
{
  get
  {
    return textFont;
  }
  set
  {
    textFont = value;
    Invalidate();
  }
}

public TextDirection TextDirection
{
  get
  {
    return textDirection;
  }
  set
  {
    textDirection = value;
    Invalidate();
  }
}

public Point StartingDisplayPoint
{
  get
  {
    return StartingDisplayPoint;
  }
  set
  {
    StartingDisplayPoint = value;
    Invalidate();
  }
}

One note about the above properties-as we have mentioned, if changing the property changes the appearance of our custom control, then it is important that we call the Invalidate() method. When running in design mode, this also causes Visual Studio .NET to redraw the control when the property changes.

Next, we need to add a Paint event handler to our custom control and modify the code as follows:

private void CustomControlWithProperties_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.FillRectangle(Brushes.White, ClientRectangle);
  PointF point = StartingDisplayPoint;
  Brush brush = new SolidBrush(TextColor);
  StringFormat sf = new StringFormat();
  if (TextFont == null)
    Text Font = new Font ("Times New Roman", 12);
  if (textDirection == TextDirection.VerticalText)
    sf.FormatFlags = StringFormatFlags.DirectionVertical;
  for (int count = 0; count < DisplayCount; ++count)
  {
    g.DrawString(DisplayText, TextFont, brush, point.X, point.Y, sf);
    if (textDirection == TextDirection.VerticalText)
      point.X += TextFont.GetHeight();
    else
      point.Y += TextFont.GetHeight();
  }
}

The code for this handler should be fairly self-explanatory. It causes the text to be painted at the appropriate location using the specified color, text direction, and font. Note the use of the StringFormat class, which, as we saw in Chapter 4, is defined in the System.Drawing namespace. This class contains information about how to draw a string, and can be passed as a parameter to the Graphics.DrawString() method. Here we use StringFormat to indicate if the text should be displayed vertically.

Now we need to build the custom control (Ctrl+Shift+B) and, if necessary, customize the Toolbox so that it contains the control. This is done in a similar fashion to the previous example, so we won't go into the details again here.

Creating a Test Application

To create a test application, add a new Windows Application project to the solution and name it TestCustomControlWithProperties. Set this to be the startup project by right-clicking on it in the Solution Explorer and clicking on Set as Startup Project.

In the Toolbox, click the Windows Forms tab. Scroll down to the bottom, and double-click on the CustomControlWithProperties entry, which will add our control to our form. Resize the control so it is substantially larger than the default size that Visual Studio .NET gives it.

At the bottom of the Properties window for this control locate the entries for all of the properties that we added to this custom control. If you click to order the properties by category, you will find all our new properties together under the Misc category. Set the properties to the following values:

Property | Value
DisplayCount | 3
DisplayText | Hello, World
StartingDisplayPoint | 6, 6
TextColor | Red
TextDirection | VerticalText
TextFont | Times New Roman, 12 point

We can notice a few things about the way that Visual Studio .NET displays the properties.

The StartingDisplayPoint property is a ‘composite' property, made up of child properties. We can either change the value of the point by entering the X and Y values separated by a comma, or we can expand the property by clicking on the + to the left of the property and entering the X and Y values separately. This behavior is automatically implemented for the Point data type. In the next chapter, when we examine design-time support in detail, we will see how to implement this for our own composite properties.

The TextDirection property is implemented with a drop-down list-Visual Studio .NET will by default do this for enums.

Both the TextColor and the TextFont properties have a custom UI Type Editor. This means that we can click on a button in the Properties window, and edit the property in a more elaborate dialog box. This dialog box can either be a drop-down style dialog box or a modal dialog box displayed in the center of the screen. The TextColor property is a drop-down style. The TextFont property is a modal dialog box style. In all these cases, Visual Studio .NET is able to customize the way you can edit the properties, because it recognizes the data types of the properties. In the next chapter, we will see how to implement your own custom UI editors for the Properties window.


Default Values of Properties

In the CustomControlWithProperties sample, all our properties were initialized with the usual default values for variables-that is a blank string, zero, a null font, or a null color. In most cases, it is better to have properties by default initialized to some meaningful values. If we want our control to be easy for other developers to use, then there are two aspects to this:

  • Making sure that our properties are initialized in the code for the control

  • Making sure that Visual Studio .NET understands the default values

The second point here is important because Visual Studio .NET, when aware of default values, is able to take several steps to make editing the properties easier:

  • If the value of the property is not the default value, then Visual Studio .NET will display the value of the property in bold.

  • Visual Studio .NET will provide a context menu for the property that allows the programmer using the control to reset the value to the default value. The programmer is able to right-click on the property value, and pick Reset Value on the context menu.

  • Visual Studio .NET can generate slightly more efficient code. We will explain how this is possible soon.

You can see this behavior very easily for the properties that are available with all forms and controls.

There are two steps that we must take to initialize our properties to default values and have Visual Studio .NET process these values properly. For example, the default value for the Text for a form or control is the empty string. If you set this property to anything else, then it is displayed in bold.

In the next section, we will develop the CustomControlWithProperties sample so that it supports these features.

Custom Control with Default Property Values Sample

To develop this sample, we first recreate the CustomControlWithProperties sample, except this time giving it (and the class that represents the control) the name CustomControlDefaultPropValues. Then, we shall make the following changes to it.

Firstly, when we declare the private member variables that contain our property values, we initialize them to the default value. Change the member variable declarations in our CustomControlDefaultPropValues class as follows:

private string displayText = "Hello, World";
    private int displayCount = 3;
    private Color textColor = Color.Red;
    private Font textFont = new Font("Times New Roman", 12);
    private TextDirection textDirection = TextDirection.HorizontalText;
    private Point startingDisplayPoint = new Point(6, 6);

Secondly, we need to use a mechanism to inform Visual Studio .NET of our default value. There are two mechanisms that we can use to do this.

The first mechanism that we can use is to set an attribute before the declaration of our property. Add the following attributes to the DisplayText, DisplayCount, and TextDirection properties:

[DefaultValue("Hello, World")]
public string DisplayText
{
  get
  {
    return displayText;
  }
  set
  {
    displayText = value;
    Invalidate();
  }
}

[DefaultValue(3)]
public int DisplayCount
{
  get
  {
    return displayCount;
  }
  set
  {
    displayCount = value;
    Invalidate();
  }
}

[DefaultValue(TextDirection.HorizontalText)]
public Color TextColor
{
  get
  {
    return textColor;
  }
  set
  {
    textColor = value;
    Invalidate();
  }
}

This approach is fine in the cases where our properties are of types whose values can be listed as a parameter in an attribute-a string, a number, or an enumeration. However, in the cases where our properties return some other type, we need to use another approach. DefaultValue won't work, for example, with Color because there is simply no way in .NET to specify a color as a parameter attribute. Instead, for these cases, we use a technique based on the Reset<PropertyName> and ShouldSerialize<PropertyName> methods. With this technique, we supply methods to the class that can reset the property to its default value, and to compare a given property to the default value. Visual Studio .NET is able to recognize these methods from their names. More specifically, we define a method, Reset<PropertyName>(), that resets the property to its default value, and a method, ShouldSerialize<PropertyName>(), which checks whether the property has its default value. For the TextColor property, these methods look like this:

public void ResetTextColor()
{
  TextColor = Color.Red;
}

public bool ShouldSerializeTextColor()
{
  return TextColor != Color.Red;
}

For the remaining properties, the Reset<>() and ShouldSerialize<>() methods are as follows:

public void ResetTextFont()
{
  TextFont = new Font("Times New Roman", 12);
}

public bool ShouldSerializeTextFont()
{
  return ! TextFont.Equals(new Font("Times New Roman", 12));
}

public void ResetStartingDisplayPoint()
{
  StartingDisplayPoint = new Point(6, 6);
}

public bool ShouldSerializeStartingDisplayPoint()
{
  return StartingDisplayPoint != new Point(6, 6);
}

Add these methods to our CustomControlDefaultPropValues class and compile the custom control.

If you are being very observant, you may have noticed that this technique is reminiscent of the old magic-name techniques that are normally regarded as bad programming practice these days. This is arguably an unfortunate design, but in this case it is the way Visual Studio .NET has been designed, so we will have to live with it,

Now we are ready to add a test application. As before, create a Windows Forms application and use the Toolbox to add our custom control to the form. Immediately after doing this, you will see that the values are not displayed in a bold font, because the current values are the default values. Change some values and Visual Studio .NET will display the property values in bold, indicating that those values are no longer the default values. Right-click on a changed value so that you can see the context menu that allows us to reset the property value. All this doesn't, of course, change the run-time behavior of the control, but it does make it a bit more convenient for us (or other developers) to use the control when writing code with Visual Studio .NET.

Important

Note that for each property, we should either use the Default Value attribute, or the Reset<PropertyName>() and ShouldSerialize<PropertyName>() methods, but not both.

More Efficient Code

We said earlier that informing Visual Studio .NET of the default property values allows it to generate slightly more efficient code. In this section, we will see how that works by examining the code generated when we change the values.

In the CustomControlDefaultPropValues sample, change some of the properties to values that are not their default values. Change the property values as follows:

Property | Value
DisplayCount | 5
DisplayText | Goodbye, World
TextColor | Lime

Now open the code editor for the form that contains our custom control to see what code the IDE has added to implement these new values. In the code editor, expand the code by clicking on the + symbol to the left of Windows Form Designer generated code:

Find the code that initializes the properties to their default values. It should look something like this:

//
// customControlDefaultPropValues1
//
this.customControlDefaultPropValues1.DisplayCount = 5;
this.customControlDefaultPropValues1.DisplayText = "Goodbye, World";
      .      .      .
      .      .      .
      .      .      .
this.customControlDefaultPropValues1.TextColor = System.Drawing.Color.Lime;
      .      .      .

We can see the three lines of code that are responsible for setting the properties, which we changed from their default values. Since the other two properties are set to their default values, Visual Studio .NET knows not to generate the code to set them. They will be set to their default values when the custom control is constructed. That is where the saving comes in: if Visual Studio .NET didn't know that these were the default values, it would supply extra redundant code to initialize the values again. For a simple control such as this one, it is a little more efficient, but for a very complex control that contained a number of constituent controls, the savings could become significant.

Note that it is important that we initialize the member variables to the same values that we indicate in either the DefaultValue attribute or the Reset<PropertyName>() and ShouldSerialize<PropertyName>() methods. This is a manual process, with no automated way to ensure that the programmer does it correctly, and if done incorrectly, would lead to some confusing behavior for the programmer using the custom control.


Focus

In this section, we will explore another aspect of making sure your controls behave correctly. In this case, we will look at what happens when the control gains or loses the focus.

We will want most of our custom controls to be able to participate in the focus mechanism of Windows Forms. This involves the following:

  • We will want to allow the user to tab to the control, as well as off it to other controls.

  • When they tab to the control, we will want some visual indication that the control has focus.

  • Many controls can be activated via a mnemonic-provided the focus is not currently held by another control that can take text input (such as a Textbox), the user can press either x or Alt+x where x is a letter that is the mnemonic for the control. This will cause focus to be transferred to the control and in some cases cause the control to activate (for example, a CheckBox will check or uncheck itself).

  • If the control is disabled, we will want its appearance to reflect that.

There is one concept that we need to be clear on before we discuss the implementation of this-that of focus cues.

Focus Cues

One aspect of the Windows user interface that many developers might not be aware of is that many controls, including the Button control, have two separate indications of focus. Consider for example the following screenshots of a button. The first screenshot shows a button that does not have the focus:

The following shows a Windows button with focus. Note the subtle difference here as there is a black outline around the button:

The following shows a Windows button with focus, but this time there is also a dotted line around the text, and the mnemonic character, O in this case, is underlined:

The difference between the last two screenshots is that for the first one, the user has only been using the mouse in the containing control. The second screenshot shows the situation if the user has also been using the keyboard to navigate between controls (for example, by pressing the Tab key). In more technical terms, in the second screenshot, the keyboard focus cues have been activated. In Windows 2000 and beyond, there is a behavior that hides keyboard focus cues until the user performs a keyboard action such as tabbing. The idea here is that if the user operates the user interface only with the mouse, by hiding keyboard focus cues the user interface is cleaner looking, and has less visual noise. Note that the dotted line appears when the user tabs between controls, but the underlining of the hot key occurs when the user hits the Alt key.

To determine if the control has focus, we use the ContainsFocus property. To determine if we should show keyboard focus cues, we use the ShowFocusCues property. If we are to have our controls behaving properly, then our drawing routines ought to reflect this usage of these two properties.

To see this behavior we do need to ensure that Windows 2000 or Windows XP is configured correctly. You can find the appropriate setting in the Display dialog, under the Effects tab. The Display dialog is activated either through the Control Panel or by right-clicking on the desktop. In Windows 2000, make sure that the checkbox titled Hide keyboard navigation indicators until I use the Alt key is checked. In Windows XP, in the Display properties dialog box, under the Appearance tab, click the Effects button. In the Effects dialog box, make sure that the checkbox titled Hide underlined letters for keyboard navigation until I press the Alt key is checked.

Steps to Implement a Focusable Control

We will next construct a simple custom control that implements focusing functionality correctly. Firstly though, we will quickly review the steps needed to write such a control. Our ‘to do' list includes the following:

  • Write a generalized drawing routine that shows focus cues (that is to say, those focus cues that are always present whether or not the user uses the keyboard) if appropriate, which also shows keyboard focus cues if appropriate, and draws the control correctly based on whether it is enabled or disabled.

  • Write a Paint event that calls our drawing routine.

  • Write an event handler for the ChangeUICues event. This is the event that is raised if Windows detects that some aspect of the focus or the focus cues may have changed.

  • Repaint our control when we get the GotFocus event that indicates the control has just taken the focus.

  • Repaint our control when we get the LostFocus event that is raised when the control has lost the focus.

  • Override the ProcessMnemonic() method. This method is defined in the Control class. It should be used to check if a key the user has pressed is the mnemonic key for that control, and if so take the appropriate action. The default implementation in Control does nothing.

  • Handle the Click event, or any desired mouse events.

  • Write any desired KeyDown or KeyUp event handlers so that the user can operate our control using the keyboard.

Let's go through these activities one at a time, and build our custom control.

Focusable Control Example

In this section, we will build a control that exhibits correct behavior for focusing. The control will act rather like a button, except that instead of using the Button class, we will code up all the drawing and responding to user events ourselves, just to show you how in principle it can be done.

We will call the control FocusableControl. Since we don't want to get bogged down in code to draw the raised or sunken appearance of buttons, the control will just look like a panel, but when it has the focus, it will display with a selected background color, which by default will be Color.LightBlue (although we will build the ability to change this color programmatically into the control):

Click To expand

In this screenshot, the text of the control has been set to the string &FOCUS, as will be done in the example we will present. This text is built into the control, but can be set when an instance of the control is created. The & in the text is not itself displayed, but indicates that the following character, F, is to be the hotkey for this instance of the control.

If the keyboard cues are to be displayed, the control will be bordered with a dotted line, just like a real button (and of course the hotkey will be underlined):

Click To expand

If the control does not have the focus, then it won't display its background in light blue, but will simply use the default control background color.

Clicking on our ‘button' will cause a dialog box to pop up saying that the control was clicked:

If you hit Enter or Space Bar while the control has the focus, you will get a similar dialog, saying that one of those keys has been pressed.

The control can also display itself grayed if it is disabled:

Click To expand

Creating the Control

As per usual, the first step is to create a new Windows Control Library, this time calling it FocusableControl. Change the UserControl1.cs file to FocusableControl.cs, the base class to System.Windows.Forms.Control, and the (Name) of the class to FocusableControl.

At the top of the FocusableControl.cs module, we want to add using statements for both the System.Drawing.Drawing2D namespace, and the System.Drawing.Text namespace since we will be using classes from these namespaces:

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Data;
using System.Windows.Forms;

Add the following private member variable to the FocusableControl class:

private Color focusColor = Color.LightBlue;

This field will contain the color to be used as the background color when the control has the focus. We will allow this color to be changed-and clearly, if it is changed, the control will need to be redrawn, so we add the following public property:

public Color FocusColor
{
  get
  {
    return focusColor;
  }
  set
  {
    focusColor = value;
    Invalidate();
  }
}

We also want the FocusColor property to display correctly in the Properties window; bold if it doesn't have its default value. We will go for the Reset/ShouldSerialize technique this time, as we demonstrated in the last example. So, we also add the following code:

public void ResetFocusColor()
{
  FocusColor = Color.LightBlue;
}

public bool ShouldSerializeFocusColor()
{
  return FocusColor != Color.LightBlue;
}

Painting the Control

Now, let's examine the code we will need to paint the control. When writing a custom control such as this, there are a number of circumstances where we need to redraw our control based on a changed state. The most effective way to do this is to write one routine that draws our control based on the current state and have this routine called whenever either something has happened that would mean the appearance of the control may have changed, or when the Paint event has been raised. So, we add the following private method to our class:

private void drawButton(Graphics g)
{
  if (ContainsFocus)
  {
    // draw in focus color
    Brush b = new SolidBrush(FocusColor);
    g.FillRectangle(b, ClientRectangle);
    b.Dispose();
  }
  else
  {
    // draw in BackColor
    Brush b = new SolidBrush(BackColor);
    g.FillRectangle(b, ClientRectangle);
    b.Dispose();
  }

  // The StringFormat class centers the text in our custom
  // control. In addition, it handles the correct drawing of
  // the mnemonic character.
  StringFormat sf = new StringFormat();
  sf.Alignment = StringAlignment.Center;
  sf.LineAlignment = StringAlignment.Center;
  if (ShowKeyboardCues)
    sf.HotkeyPrefix = HotkeyPrefix.Show;
  else
    sf.HotkeyPrefix = HotkeyPrefix.Hide;
  if (Enabled)
  {
    // If the control is enabled, the color of the text is ForeColor.
    Brush fb = new SolidBrush(ForeColor);
    g.DrawString(Text, Font, fb, ClientRectangle, sf);
    fb.Dispose();
  }
  else
  {
    // If the control is not enabled, first draw the text in white,
    // offset one pixel up and to the left. Then draw the text in
    // DarkGray.
    g.TranslateTransform(−1, −1);
    g.DrawString(Text, Font, Brushes.White, ClientRectangle, sf);
    g.ResetTransform();
    g.Drawstring(Text, Font, Brushes.DarkGray, ClientRectangle, sf);
  }

  // Draw a dotted line inside the client rectangle.
  if (ShowFocusCues && ContainsFocus)
  {
    Rectangle r = ClientRectangle;
    r.Inflate(−4, −4);
    r.Width--;
    r.Height--;
    Pen p = new Pen(Color.Black, 1);
    p.DashStyle = DashStyle.Dot;
    g.DrawRectangle(p, r);
  }
}

There is quite a lot going on in this method. We start off by testing to see if the control has the focus, and if so shade in the background color:

if (ContainsFocus)
{
   .   .
   .   .
   .   .

ContainsFocus is a Boolean property that our class inherits from the Control class. It returns true if the control has the focus.

The next part of the code is to do with displaying the text for the control. For this, we use that helper class, System.Drawing.StringFormat, again. We set the Alignment and LineAlignment properties of the StringFormat object to instruct DrawString() to center the text horizontally and vertically. More interesting is the code to control underlining of the hotkey:

if (ShowKeyboardCues)
  sf.HotkeyPrefix = HotkeyPrefix.Show;
else
  sf.HotkeyPrefix = HotkeyPrefix.Hide;

ShowKeyboardCues is another Boolean property that is implemented in the Control class. This one returns true if the system detects that we should be showing keyboard cues-in other words, if the user has started using keys to navigate round the form. If keyboard cues are to be shown, then we will want to underline the hotkey (or mnemonic key) for this control-and we can get Graphics.DrawString() to do this by setting the StringFormat.HotkeyPrefix property to the enumerated value HotkeyPrefix.Show. (The HotkeyPrefix enumeration is defined in the System.Drawing.Text namespace.) Graphics.DrawString() will identify the hotkey as the first character that is preceded by a & in the text it is asked to print.

Having set up the StringFormat object, we then draw the string-in the normal foreground color if the control is enabled, grayed if it is disabled.

Finally, if keyboard cues are enabled, we need to draw the dotted line around the control. That is the purpose of the final if block in the drawButton() method:

// Draw a dotted line inside the client rectangle,
if (ShowFocusCues && ContainsFocus)
{
   .   .
   .   .
   .   .

If the control has the focus when this routine draws the background color of our custom control, it draws it in the FocusColor regardless of whether we should show the keyboard focus cues or not. However, we draw the dotted line inside the client rectangle only if both ShowFocusCues and ContainsFocus are true.

Invoking the Method to Paint the Control

Now that we have defined our drawButton() method, we need to ensure it is called at appropriate times. The most obvious case is when the Paint event is raised. So, we will add a Paint event handler, in the usual way by clicking on the Events button in the Properties window of the designer view and then double-clicking on the Paint event. The code we need to add to our handler is as follows:

private void FocusableControl_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
  {
    drawButton(e.Graphics);
  }

We also need to add handlers for those times when the appearance of the control changes. This can happen in the following cases:

  • The user interface cues change

  • The control gains or loses focus

  • The control is enabled or disabled

  • The mnemonic key is pressed-we will deal with this case in the next section

There are other cases, such as when the control is shown or hidden, but those are already taken care of by the Control class. The above list gives those cases that we will have to deal with in our code.

First, let's cover the case for which user interface cues change. At various times, Windows Forms will determine that the user interface cues should be changed. As an example, if keyboard focus clues are not currently being shown, and the user presses a key, Windows Forms may decide that keyboard cues should now be shown. When this happens, Windows will raise the ChangeUICues event. We implement an event handler for this as follows:

private void FocusableControl_ChangeUICues(
  object sender, System.Windows.Forms.UICuesEventArgs e)
{
  if (e.ChangeFocus | | e.ChangeKeyboard)
  {
    Graphics g = CreateGraphics();
    drawButton(g);
    g.Dispose();
  }
}

Note that we have chosen to implement this handler by calling drawButton() directly. If we preferred, we could have simply called Control.Invalidate() to get a Paint event raised, which would have the same effect.

Now, for the cases when the control gains or loses the focus. For simple cases, the Control class provides Enter and Leave events. However we won't use those-instead we will provide event handlers for the events GotFocus and LostFocus. These events are similar to Enter and Leave but work at a lower level and it is recommended that you use those events if you are working with stuff like controlling the UI cue painting event manually-as we are doing here.

Now, ordinarily, when we implement an event for our custom control, we can have Visual Studio .NET do most of the housekeeping-we can open the Design window for our custom control, and then open the Properties window. We can click on the Events button (the small yellow lightning bolt) to see the list of events. We can then double-click on the event that we need to implement, and the IDE will automatically create an empty event handler, and automatically register our event handler in the initializeComponent() method, which is in the Component Designer generated Code section in the code editor for our control.

However, these two events are different. These events have the Browsable attribute set to false, so we can't see these events in the Properties window, reflecting the fact that these are relatively low-level events that you should only be providing handlers for if you are doing fairly advanced stuff. Indeed, if you don't take care with the code that handles these events you can cause our application to stop responding. In these two events, we are supposed to do one thing, and one thing only, which is to modify the appearance of our custom control.

Since the Browsable property is set to false, we need to register our event handler manually, which means modifying the code in the constructor to the control.

public FocusableControl()
{
  // This call is required by the Windows.Forms Form Designer.
  InitializeComponent();

  this.GotFocus += new EventHandler(FocusableControl_Got Focus);
  this.LostFocus += new EventHandler(FocusableControl_Lost Focus);
}

Now we can implement the event handlers:

private void FocusableControl_GotFocus(
  object sender, System.EventArgs e)
{
  Graphics g = CreateGraphics();
  drawButton(g);
  g.Dispose();
}

private void FocusableControl_LostFocus (
  object sender, System.EventArgs e)
{
  Graphics g = CreateGraphics();
  drawButton(g);
  g.Dispose();
}

Enabling the Mnemonic Key

When the user presses a key, then Windows Forms gives each control the chance to check if that key is the mnemonic for that control and, if so, to process the keystroke. Windows does this by calling the virtual method, Control.ProcessMnemonic() against each control, passing it as a parameter the character that has been pressed. If all controls decline to process it, then the event ‘bubbles up' to container controls and forms, until a control indicates that it has accepted the key press by returning true from ProcessMnemonic(). The default Control implementation of ProcessMnemonic() does nothing and always returns false.

To process the mnemonic keystroke, we override the ProcessMnemonic() method of our control. If our control is both enabled and visible, and if the character that was pressed is the mnemonic character for our control, then our method should take the appropriate action, and return true. Otherwise, our method should return false. Add the ProcessMnemonic() method to our class, as follows:

protected override bool ProcessMnemonic (char charCode)
{
  if (Enabled && Visible && IsMnemonic(charCode, this.Text))
  {
    // Perform action associated with mnemonic.
    // Moves focus to our control.
    Focus();
    Graphics g = this.CreateGraphics();
    drawButton(g);
    g.Dispose();
    MessageBox.Show("Mnemonic pressed");
    return true;
  }
  return false;
}

One item of note is the use of the IsMnemonic() method. This is a static method defined in Control, and it compares the character and string passed in to see if the character is the hotkey for that string (that is to say, the first character that is preceded by an &). If it is then IsMnemonic() returns true, if not it returns false. We can pass the character code and the text of our control to this method-this does depend on our having set the text of the control to something that identifies a hotkey-but as earlier remarked, we will do so in our code, since we will give our control the text, &FOCUS. The IsMnemonic() method, along with the special mnemonic functionality in the StringFormat class makes it easy for us to implement mnemonic functionality.

Note that because the system will automatically call ProcessMnemonic() against the controls on the form when the user presses a key, there is no need for us to explicitly write any code to invoke the method.

Responding to User Events

While we are not going to implement full button-style functionality with mouse events (in other words, pressing, releasing, trapping the mouse, and so on), let's add an event handler for the Click event. The Click event is browsable, so we can add the event handler in the automated way. Here is the Click event handler:

private void FocusableControl_Click(
  object sender, System.EventArgs e)
{
  Focus();
  MessageBox.Show("Control clicked");
}

The first thing that this method does when it gets this event is to move the focus to our control. It then takes whatever action it should take when the control is clicked. For now, the method puts up a dialog box.

Let's also add an event handler for keyboard events, so that when the control has focus, the user can press the Space Bar or the Enter key. There are three keyboard events, each for a different purpose:

Event | Purpose
KeyDown | Occurs once when the key is pressed. This event is used to operate controls that are not used for editing text, such as buttons, tree controls, and so on.
KeyPress | Occurs when the key is pressed. If the user holds the key down, the event repeats at the rate specified in the Keyboard Properties dialog box, which is opened from the Control Panel. This event is used in a control that edits text.
KeyUp | Occurs once when the key is released. This event can be used in conjunction with the KeyDown event to give some type of visual feedback when the user presses and releases keys. As an example, if we tab to a button in a dialog box, and press the Space Bar, the button is visually pressed. When we release the space bar, the button is visually released.

For the purposes of this control, we want to implement a handler for the KeyDown event:

private void FocusableControl_KeyDown(
  object sender, System.Windows.Forms.KeyEventArgs e)
{
  if (e.Alt == false &&
    e.Control == false &&
    e.Shift == false &&
    (e.KeyCode == Keys.Space | |
    e.KeyCode == Keys.Enter))
  {
    MessageBox.Show("Space or Enter pressed");
    e.Handled = true;
    return;
  }
}

In this event handler, we need to make sure that none of the Alt key, the Ctrl key, or the Shift key is down. If this is true, and the user pressed either the Space Bar or the Enter key, we take the appropriate action.

Creating a Test Application

Our FocusableControl control is now ready, so we need to write a container form to test it with. To our existing project, we should add a new Windows Application named TestFocusableControl and set it as the startup project using the usual procedure. Our test harness will be a Windows Forms application that contains an instance of FocusableControl, along with two real buttons. One of the buttons is used to show or hide the control, the other is used to enable or disable it. Hence when running, the application will look something like this (when keyboard cues are in use):

Click To expand

Once you have created the project, you should use the Visual Studio .NET design view to add a FocusableControl object to the form from the Toolbox. Resize it so it is a suitable size and set its Text property of the control to &FOCUS. Then, add two Buttons to the form.

Set the following properties for the first Button:

Property | Value
(Name) | btnEnableDisable
Text | &Disable

Set the following properties for the second Button:

Property | Value
(Name) | btnShowHide
Text | &Hide

Double-click on the btnEnableDisable button, and modify the Click event as follows:

private void btnEnableDisable_Click(object sender, System.EventArgs e)
{
  btnEnableDisable.Text =
    focusableControl1.Enabled ? "&Enable" : "&Disable";
  focusableControl1.Enabled = !focusableControl1.Enabled;
}

Double-click on the btnShowHide button, and modify the Click event as follows:

private void btnShowHide_Click(object sender, System.EventArgs e)
{
  btnShowHide.Text = focusableControl1.Visible ? "&Show" : "&Hide";
  focusableControl1.Visible = !focusableControl1.Visible;
}

Now we can compile and run the test application, and see the behavior that we have implemented.

  • When we start our test application, we can see that our control has the focus. It has the focus first because it was the first control that we added to our application.

  • If we tab, then we can see our control lose and gain focus-and, since we are using the keyboard to move between controls, we will see the keyboard cues appear on both our control and the real buttons.

  • If we Disable the control, we can see the disabled appearance.

  • While disabled, we also can see that our control no longer participates in the tabbing sequence.

  • We can click on our control, which both moves the focus to it, and shows a message box.

  • We can hide or show our control.

  • When our control has the focus, we can press the Space Bar or Enter key, and we will see a message box.

  • If our control is enabled and visible, we can press F, and we will see a message box. If our control is disabled or invisible, the mnemonic is no longer active.

One more note. If you start the test application from Visual Studio .NET using the keyboard, it starts with the keyboard focus cues visible. If you want to see the test application without the keyboard focus cues, start it using the mouse. From the menu bar, select Debug | Start. The application starts, and the keyboard focus cues are hidden. Now, press the tab key, and we can see the keyboard focus cues appear. We can see that our custom control has the same focus behavior as the native Windows buttons.


Generating Events

As we have mentioned earlier, events form an important part of Windows Forms architecture. In the samples we have presented so far, we have been freely using some of the basic events defined by Microsoft-Paint for example. However, you may also find that you need to define and raise your own events that are relevant to your custom controls. In this section, we will develop a sample that demonstrates how to do that. We will first review the basic principles of delegates and events before moving on to modifying the previous FocusableControl example and its test application to include a custom event.

Overview of Delegates and Events

Events are implemented via delegates, so first, we will quickly review delegates.

A delegate is a reference type that refers to either a static or instance method of a class. It is similar to function pointers, but has some advantages over them. Function pointers could only refer to a static method, or a method not contained in a class, whereas delegates can refer to instance methods as well.

When we instantiate a delegate, we can, and often do, instantiate it with a reference to both an object and a method. The delegate will then execute in the context of that object-the code in the method referred to by the delegate can refer to the this object, and the this object is the object with which we instantiated the delegate.

Another important difference between pointers to functions and delegates is that we can compose delegates of more than one method. This is sometimes called a multi-cast delegate. When we call the delegate, it will invoke more than one method, which means that we can have more than one event handler for a given event. This is sometimes useful.

It is invalid to declare a multi-cast delegate that returns a value. If each method in a delegate returns a value, what would be the return value of a call to the delegate?

To define, declare, and use a delegate is a three-step process:

  3. Write a delegate definition.

  4. Declare and instantiate an object of the type of our delegate definition.

  5. Invoke the method(s) to which the delegate refers.

A delegate definition is as follows:

public delegate void MyDelegate(Object obj, EventArgs e);

We can declare a delegate as a member variable of our class, like this:

public MyDelegate MyEvent;

If we have an event handler declared like this:

private void MyEventHandler(Object obj, EventArgs e)
{
  /*
  code to handle the event goes in this method.
  */
  Console.WriteLine("in MyEventHandler");
}

then we can initialize our delegate variable like this:

this.MyEvent += new MyDelegate(this.MyEventHandler);

Having declared our delegate, we could now invoke it:

Object obj = this;   // This code is operating in the context of an object
                     // that is going to raise the event.
EventArgs args = new EventArgs();  // Initialize args as appropriate
                                   // after constructing it.
MyEvent(obj, args);  // Invoke the delegate.

Events are a special type of a multi-cast delegate. It is slightly different from a multi-cast delegate in two ways. Firstly, there are certain restrictions on the signatures of event handlers. Secondly, C# has some additional syntactical constructs to make it easier to use events.

The only difference between declaring an event and a delegate is the use of the event keyword in the member variable declaration:

public event MyDelegate MyEvent;

This use of the event keyword enables Visual Studio .NET to put MyEvent in the Events pane of the Properties window.

Events Example

Now that we have reviewed delegates and events, let's add an event to our FocusableControl example. At present, the event handlers for when the control is clicked on, or the Space Bar, mnemonic, or Enter key is pressed simply display message boxes. We will modify these so that they raise an event that we will have defined. It is then up to the containing form-or for that matter any other interested party-to handle this event however it chooses, which for our example will be once again by displaying a message box. Bear in mind that this means we will be adding a substantial amount of code, which for this particular example isn't actually going to change much in terms of what the application does. However, the real significance is the way that linking these actions into the event architecture allows other controls or classes to handle the event if required. Using events also in general means that in a multithreaded application, the event handler will always be run on the user interface thread.

We will thus modify our control so that it generates a custom event, called FocusableControlEvent. This will allow us to send additional information with the event, such as whether the event was generated with the mouse, the Space Bar, the Enter key, or the mnemonic key.

To start with, we need to recreate the FocusableControl project-except that this time we will name the class FocusableControlWithEvents. Then, we will make the following changes to the code.

To start with we need an EventArgs-derived class that can supply custom information to the event handlers for the event. We will call the class FocusableControlEventArgs and here is its definition:

public class FocusableControlEventArgs : EventArgs
{
  public enum Focus ableControlEventType
  {
    Mouse,
    SpaceBar,
    Enter,
    Mnemonic
  }

  private FocusableControlEventType eventType;

  public FocusableControlEventType EventType
  {
    get
    {
      return eventType;
    }
    set
    {
      eventType = value;
    }
  }

  public FocusableControlEventArgs(FocusableControlEventType et)
  {
    eventType = et;
  }
}

We see from this code that we define an enum that describes the possible reasons why the event might have been raised, and define a property in the FocusableControlEventArgs class that sets or retrieves an instance of this enum as the EventType.

Next, we define the delegate type that will represent the event. Add this delegate definition to the namespace:

public delegate void FocusableControlEventHandler(
  object sender, FocusableControlEventArgs e);

Now, we modify the declaration of the FocusableControl class to include the event type FocusableControlEventHandler-a public member variable named FocusableControlClick:

public class FocusableControlWithEvents : System.Windows.Forms.Control
{
  private Color focusColor = Color.LightBlue;
  public event FocusableControlEventHandler FocusableControlClick;

What we have just seen is the entire infrastructure necessary for us to generate events. Now, let's modify the control to raise the events at the appropriate places instead of putting up the dialog box.

Modify the ProcessMnemonic() method by adding the following:

protected override bool ProcessMnemonic(char charCode)
{
  if (Enabled && Visible && IsMnemonic(charCode, this.Text))
  {
    // Perform action associated with mnemonic.
    // Moves focus to our control.
    Focus();
    Graphics g = this.CreateGraphics();
    drawButton(g);
    g.Dispose();
    // Raise the event.
    FocusableControlEventArgs args = new FocusableControlEventArgs(
      FocusableControlEventArgs.FocusableControlEventType.Mnemonic);
    if (FocusableControlClick != null)
      FocusableControlClick(this, args);
    return true;
  }
  return false;
}

Note that this code raises the event. Whether any code is run in response to that depends on whether any control opts to handle that event. We won't write any event handler in the FocusableControlWithEvents class itself, but when we place the control in a test form, we will have the test form implement a handler.

It is important that we test FocusableControlClick and make sure that it is not null. It will be null until an event handler is added to it. This will prevent our control from generating an exception in the case where the developer using the control puts the control on a form, doesn't write an event handler for the control, runs the test application, and clicks the control.

Important

When implementing other events, verifying that the delegate is not null is even more important. If we implement an event that fires during the Paint event for the custom control, we can't even bring up the design window without an exception unless we verify that the delegate is not null before calling it.

Now modify the Click event as follows:

private void FocusableControl_Click(object sender, System.EventArgs e)
{
  Focus();
  // Generate the event.
  FocusableControlEventArgs args = new FocusableControlEventArgs(
    FocusableControlEventArgs.FocusableControlEventType.Mouse);
  if (FocusableControlClick != null)
    FocusableControlClick(this, args);
}

The KeyDown event contains the code for generating the final two types of events, SpaceBar, and Enter:

    private void FocusableControl_KeyDown(object sender,
System.Windows.Forms.KeyEventArgs e)
    {
      if (e.Alt == false &&
        e.Control == false &&
        e.Shift == false &&
        (e.KeyCode == Keys.Space | |
        e.KeyCode == Keys.Enter))
      {
        // Generate the event.
        FocusableControl Event Args args;
        if (e.KeyCode == Keys.Space)
          args = new FocusableControlEventArgs(
            FocusableControlEventArgs.FocusableControlEventType.SpaceBar);
        else
          args = new FocusableControlEventArgs(
            FocusableControlEventArgs.FocusableControlEventType.Enter);
        if (FocusableControlClick != null)
          FocusableControlClick(this, args);
        e.Handled = true;
        return;
      }
    }

Next, compile the custom control. We need to modify our test application to handle our new event, but before the event will be available in the Properties window, we need to compile the custom control.

Create a Test Application

To create a test application, we need to create a Windows Form as before, and insert the same code into it as for the TestFocusableControl application earlier, with one exception; instead of adding a FocusableControl to the form, add a FocusableControlWithEvents.

Then, we need to add a handler for the FocusableControlClick event. As usual, select the control in the design view, and double-click on the FocusableControlClick event in the Properties window. Visual Studio .NET will create an empty event handler, and set up the wiring for the event handler in the InitializeComponent() method. Modify the event handler like so:

private void focusableControlWithEvents1_FocusableControlClick(
  object sender, FocusableControl.FocusableControlEventArgs e)
{
  MessageBox.Show("Source of event is: " +  e.EventType);
}

Compile and run the application. We can now operate the control so that it generates its event in the four possible ways.


Deriving from an Existing Control

Another approach to building a custom control is to derive from an existing Windows Forms control. This avenue allows us to do the following:

  • Make use of the existing functionality of a control

  • Extend the control by adding new properties

  • Change the functionality by handling some of the events of the control

  • Extend the control by generating new events

Some scenarios where this approach works well include:

  • We can do domain analysis on validations required in our application. After we determine the set of validations that are possible, we can, for example, write a single parameterized custom control that is derived from the TextBox control. We can specify the data validations to be applied to each instance of our custom TextBox control via properties.

  • We could derive from the DataGrid control, and make an elaborate control similar to a spreadsheet. Perhaps the data would come from a DataSet, with an addition of some calculations to compute additional values. The calculations could be specified via properties.

  • A more domain-specific application might be a situation where we have a large number of listboxes or combination boxes, all of which display one or another subset of the same list of values. We could derive from the ListBox control, and add a property or two that would allow the user of the control to indicate which subset should populate the ListBox.

  • In a similar vein, you might derive classes from Tree View and Tree Node to implement specialist treeviews, such as a tree view used to navigate through the file system.

In this example, we will derive from the TextBox control, and add a property that allows us to specify a simple validation.

NumberTextBox Example

In this example, we will design a TextBox that is supposed to hold a numeric value, and which performs data validation to make sure that its contents are in a given range. The upper and lower bounds of this range are defined by properties and can be set by client code. If the client code sets the upper range to be less than the lower range then the control will check that its contents lie outside the range. The following table, which lists the actual limits we will use in our test application, should make it clear what we mean:

Lower limit | Upper limit | Meaning
0.0 | 100.0 | Text in TextBox must be a number between 0.0 and 100.0
200.0 | 0.0 | Text in TextBox must be a number that is either less than zero or greater than 200.0

If, while validating, the control detects that its contents are out of range (or not a number at all) then it will change its own background color to yellow. The control will also make available a Boolean property, ContentsInRange, which indicates whether it contains valid data, and which can be used by client code to determine whether to take other actions. In the example we develop, we will place a couple of these controls on a form and have the form display an error symbol if the contents of the boxes are out of range. Hovering the mouse over this error symbol causes a tooltip displaying an error message to be displayed. The error symbol and associated tooltip are supplied not by our NumberTextBox class, but by the class, System.Windows.Forms.ErrorProvider, supplied as part of the .NET Framework class library.

The full running code will look like this:

Click To expand

In the above screenshot, the first TextBox contains valid contents (as per the above table, it accepts a number between 0 and 100), while the second TextBox contains an invalid number. Validation is performed when the Validating event is raised for each NumberTextBox. The Validating event is raised when the control loses the focus.

Note that this is a very simple example designed to demonstrate the principles of deriving from an existing control. Obviously, in a commercial application, you possibly wouldn't bother writing code to derive from an existing control unless you had some much more significant feature to add to the control-for minor features it is usually easier to just use the existing control and manipulate it by methods in the containing form or control. Also bear in mind that if in one of your projects you do want a control that allows the user to type in only numbers, then you may find the existing System.Windows.Forms.NumericUpDown control fits your needs better.

Creating the Control

Create a new Windows Control Library in Visual Studio .NET and name it NumberTextBox. In the Solution Explorer, right-click on UserControl1.cs and rename it to NumberTextBox.cs. Then, change the base class of the NumberTextBox class to the System.Windows.Forms.TextBox class. Finally, change the (Name) property of the class to NumberTextBox.

We are going to add two string properties to the TextBox control; RangeText, and ValidationErrorMessage. The RangeText property contains the low and high limits of the allowed number range separated by a comma. We are choosing to supply this property as a single string rather than as two separate numeric properties because we want the two limits of the range to be settable as one single property. This isn't particularly important at the moment, but will become important in the next chapter when we develop this sample further to add more design-time support to it. The ValidationErrorMessage property will return a string describing the problem if the text in the control is out of range. This property is read-only.

However, before we start editing the code for the control, we will define a class that represents the exception that will be thrown if any client code attempts to set an invalid value for the RangeText, for example, a non-numeric string. So, within the namespace, add this class to the file:

public class InvalidRangeTextException : ApplicationException
{
  public InvalidRangeText Exception(string s) : base(s)
  {
  }
}

Now we will edit the control itself. First add three private member variables to the NumberTextBox class. These define the range in both text and numeric form, and set default values:

private string rangeText = "0,100";
private double low = 0, high = 100;

Now we will define the property that allows the range to be set or examined. Notice that we use the DefaultValue attribute to inform Visual Studio .NET of the default value:

[DefaultValue("0,100")]
public string RangeText
{
  get
  {
    return rangeText;
  }
  set
  {
    if (value == null)
      throw new InvalidRangeTextException("Range text cannot be null");
    string[] v = ((string)value).Split(new char[] {','});
    if (v.Length != 2)
      throw new InvalidRangeTextException(
       "Range text should have format <f>,<f> where
       <f>= floating-point number");
    try
    {
      low = double.Parse(v[0]);
      high = double.Parse(v[1]);
    }
    catch (FormatException)
    {
      throw new InvalidRangeTextException(
        "Validation parameters should have form <f>,<f> where
        <f>=floating point number");
    }
    rangeText = value;
  }
}

The get accessor for this property is fairly trivial. The set accessor is more complex since we need to verify that the text to which the range is being set is of the form <number>,<number>, and throw an exception if it isn't. The above code does this and also stores the limits in the fields low and high, so we can easily validate data later on.

You may wonder what happens if you try to set the string to an invalid value in the Properties window. In fact, when setting a property, Visual Studio .NET actually calls the set accessor against the control. As you will be able to verify when we have completed the sample, Visual Studio .NET will automatically catch any exception and display a dialog box telling you the value is invalid.

We next need to add the property that tells us whether the text typed into the TextBox is in range:

[Browsable(false)]
public bool ContentsInRange
{
  get
  {
    double textBoxValue;

    // Check value is a number.
    try
    {
      textBoxValue = Double.Parse(this.Text);
    }
    catch (FormatException)
    {
      return false;
    }

    // If high < low then check number is OUT of range.
    if (high < low)
    {
      if(textBoxValue < low && textBoxValue > high)
        return false;
      else
        return true;
    }
    // high >= low then check number is IN range
    if (textBoxValue < low | | textBoxValue > high)
      return false;
    return true;
  }
}

The first item of interest in this code is that we apply a new attribute, Browsable, to it. We mentioned earlier in the chapter that applying this attribute and setting it to false will prevent Visual Studio .NET from displaying this property in the Properties window, but this is the first time we have seen this attribute in action. Clearly, testing whether the contents are in range is a purely run-time action so it is pointless displaying this property in the Properties window at design-time. If we didn't apply this attribute, the property would get displayed, although it would be grayed out since it is a read-only property.

As far as implementation of this property is concerned, we first try to convert the contents of the TextBox value to a double. If the TextBox contains a string like hello, this will throw an exception, which we must then catch. Otherwise, we can check if the number is in the required range and return true if it is.

The final property we need to code up is the error message to be returned if the contents are out of range:

[Browsable(false)]
public string ValidationErrorMessage
{
  get
  {
    if (ContentsInRange)
      return "";
    if (high >= low)
      return "Value must lie between " + low.ToString() +
        " and " + high.ToString();
    else
      return "Value must NOT lie between " + high.ToString()
        " and " + low.ToString();
  }
}

Note that this property returns an empty string if the contents are valid, since clearly in this case, there is no error message to be displayed. We also set the Browsable attribute to false to prevent display of this attribute in the Properties window.

The final bit of code we need to add is a Validating event handler that turns the background of the TextBox yellow if the contents are invalid (or returns it to white if the contents are correct). Click on the design window, press F4 to open the Properties window, and click on the Events button in the toolbar of the Properties window. Locate the Validating event, and double-click on it to create an empty event handler for it.

Modify the event handler to add code to perform the validation, like so:

private void NumberTextBox_Validating(
  object sender, System.ComponentModel.CancelEventArgs e)
{
  if (Content sInRange)
  {
    this.BackColor = Color.White;
  }
  else
  {
    this.BackColor = Color.Yellow;
  }
  e.Cancel = false;
}

Notice that this event handler is passed a CancelEventArgs instance. CancelEventArgs is .NET's way of allowing a handler to ‘cancel' an event, by setting the CancelEventArgs.Cancel property to true. This mechanism is used for example, to prevent the closing of an application if there is unsaved data. For the case of validating, in many cases we would opt to cancel the event if validation fails. However, we won't do that here. The reason is that canceling a Validating event would prevent focus from leaving the control until the user corrects the contents of the textbox. This may be appropriate for some applications but here what we want is an app that displays an error symbol but still lets the user navigate around the form. Hence, we always set e.Cancel to false even if validation fails.

That's all there is to our custom control. Compile it and add it to the Toolbox window. All we are left to do is to build an application to test the control.

Creating a Test Application

Add a Windows Application to the project, call it TestNumberTextBox, and set it as the startup project. As usual, if necessary, make sure that the NumberTextBox control is in the Toolbox.

In the Design window, place two NumberTextBox controls on the test application's form, along with two Label controls to label the textboxes. Use the Properties window to clear the Text property for both NumberTextBoxes. For the first one, leave the RangeText at its default value of 0,100. For the second one, set it to 200,0.

Next, use the toolbox to add an ErrorProvider control to the form. Since the ErrorProvider doesn't, by default, have an appearance, it will be represented by an icon below the form in the design view. We will use this ErrorProvider to display an error icon, if necessary, from the event handlers that we will write for the Validation events for each of the two NumberTextBoxes. For this case, we will use the same event handler for both controls, using the sender parameter passed to the handler to distinguish which control raised the event. So, in the code view, we type in the following code in the Form1 class:

private void validationTextBox_Validating(
  object sender, System.ComponentModel.CancelEventArgs e)
{
  NumberTextBox tbSender = (NumberTextBox)sender;
  if (!tbSender.ContentsInRange)
  {
    this.errorProvider1.SetError(
      tbSender, tbSender.ValidationErrorMessage);
  }
  else
  {
    this.errorProvider1.SetError (tbSender, "");
  }
}

This code uses the ErrorProvider.SetError() method, which takes a reference to a control and a string as a parameter. Provided the string is not empty, this method places that red error icon by the control and adds a tooltip containing the given error message. If the string is empty, then SetError() removes any existing error icon from the control. Notice how we retrieve the error message from the NumberTextBox itself.

To attach this code to the Validating events, use the Properties window to locate the Validating event for each NumberTextBox in turn. Instead of double-clicking to add a new event handler method, click on the arrow to open the drop-down listbox, and select the name of the method that we have just typed in:

Click To expand

Notice how the Validating event for each control now has two event handlers; one within the NumberTextBox class itself, which paints the control yellow if there is an error, and one within the containing form, which adds the error icon. This nicely shows how event-based architecture allows different classes to add their own processing to any event.

The application is now ready to compile and run. Try out the NumberTextBox controls by entering a variety of values.

In the next chapter, we will enhance this control in a couple of ways to make it more convenient to specify the validation parameters at design-time.


Composite Custom Controls

With this technique, we can compose a custom control with any number of constituent controls.

Example uses of this technique are:

  • If we were building an accounting application, we would want to implement some standard behavior for entering transaction distributions.

  • A filtering listbox. We can implement a custom control that has the same user interface as a standard listbox, with the addition of a textbox that allows us to enter filtering information. As we enter filtering information into the textbox, we update the contents of the listbox to reflect the filter.

  • If we were building a database application, we would want to have the navigation and database operation controls be identical for all windows. This can be a mechanism to enforce consistency across a development team or throughout an entire company.

We can declare properties for our composite control, which allows us to parameterize at a higher-level of abstraction. As an example, suppose we had several buttons that navigate through and operate on a database. We could have a property that allows us to define one of the following options:

  • This window is allowed to add, change, delete, and search for records

  • This window is only allowed to change and search for records

  • This window is only allowed to search for records

Based on the value of this property, we could hide controls, move them around, change their text, and so on. A lot of functionality would be encapsulated in a single property.

We can add our own high-level events. We can handle events of the constituent controls, and if so desired, ‘bubble' them up to the user of the composite custom control.

To demonstrate this technique, we will build a custom control that contains four buttons. We will have a property that contains the database table name, and change the text of the buttons per this property. We will define an event that indicates whether the user asked to add, change, delete, or search.

DataButtons Example

Create a new Windows Control Library named DataButtons and rename UserControl1.cs to DataButtons.cs. In the code for DataButtons.cs, change the name of the class from UserControl1 to DataButtons. Similarly, change the name of the constructor for the class from UserControl1 to DataButtons.

As always, when Visual Studio .NET builds a new project based on the Windows Control Library template, it defines our custom control with UserControl as the base class. In this case, we are not going to change it.

Switch to the Design view and resize the custom control, making it shorter and wider so that it can accommodate four Buttons placed horizontally across it. Drag four Buttons onto the custom control, and position them like this:

Click To expand

Set the following properties for the Buttons:

Property | button1 | button2 | button3 | button4
Text | Add | Change | Delete | Search
Name | btnAdd | btnChange | btnDelete | btnSearch

Within the DataButtons namespace, declare the following class and delegate for the event that our control will generate:

public class DataButtonsEventArgs : EventArgs
{
  public enum DataButtonsEventType
  {
    Add,
    Change,
    Delete,
    Search
  }

  private DataButtonsEventType eventType;

  public DataButtonsEventType EventType
  {
    get
    {
      return eventType;
    }
    set
    {
      eventType = value;
    }
  }

  public DataButtonsEventArgs(DataButtonsEventType et)
  {
    eventType = et;
  }
}

public delegate void DataButtonsEventHandler(
  object sender, DataButtonsEventArgs e);

Add the following member variables to the DataButtons class:

private string tableName;
public event DataButtonsEventHandler DataButtonsEvent;

Add the following public property to the DataButtons class:

public string TableName
{
  get
  {
    return tableName;
  }
  set
  {
    tableName = value;
    btnAdd.Text = "Add " + tableName;
    btnChange.Text = "Change " + tableName;
    btnDelete.Text = "Delete " + tableName;
    btnSearch.Text = "Search " + tableName;
  }
}

In the Design window, double-click on the Add button and add the following event handler:

private void btnAdd_Click(object sender, System.EventArgs e)
{
  // Generate the event.
  DataButtonsEventArgs args = new DataButtonsEventArgs(
    DataButtonsEventArgs.DataButtonsEventType.Add);
  if (DataButtonsEvent != null)
    DataButtonsEvent(this, args);
}

Similarly, double-click on the Change button and modify the event handler:

private void btnChange_Click(object sender, System.EventArgs e)
{
  // Generate the event,
  DataButtonsEventArgs args = new DataButtonsEventArgs(
    DataButtonsEventArgs.DataButtonsEventType.Change);
  if (DataButtonsEvent != null)
    DataButtonsEvent(this, args);
}

Likewise for the Delete button:

private void btnDelete_Click(object sender, System.EventArgs e)
{
  // Generate the event.
  DataButtonsEventArgs args = new DataButtonsEventArgs(
    DataButtonsEventArgs.DataButtonsEventType.Delete);
  if (DataButtonsEvent != null)
    DataButtonsEvent(this, args);
}

and finally for the Search button:

private void btnSearch_Click(object sender, System.EventArgs e)
{
  // Generate the event.
  DataButtonsEventArgs args = new DataButtonsEventArgs(
    DataButtonsEventArgs.DataButtonsEventType.Search);
  if (DataButtonsEvent != null)
    DataButtonsEvent(this, args);
}

Compile it and add it to the Toolbox.

Create a Test Application

Add a new Windows Application to the current solution and call it TestDataButtons. As per the usual procedure, set it as the startup project and add a reference to the DataButtons project.

Drag a DataButton control from the Toolbox onto the form, resize it so you can see all of the buttons, and set the TableName property to Customers. Notice that the text of the buttons changes.

Add a couple of TextBox controls to the form, so that there are controls to which we can tab from our buttons. Select the DataButtons control in the form and open the Properties window. Click on the Events button in the toolbar. Scroll to find the DataButtonsEvent and double-click on it to create an empty event handler. Modify the event handler as follows:

private void dataButtons1_DataButtonsEvent(
  object sender, DataButtons.DataButtonsEventArgs e)
{
  MessageBox.Show(e.EventType.ToString());
}

Compile and run the application. We can then tab between the controls, click the buttons, and so on.


Designing Components and Custom Controls

We have now completed working through different custom controls. For the rest of this chapter, we will examine some of the principles of designing good controls.

When it comes time to do the architectural design of your planned components and controls, there are two questions to ask:

  • What components and controls should you build?

  • What should the programming interface be with our components and controls?

The question about what your programming interface should be can then further be broken up into three questions:

  • What properties do you need?

  • What methods do you need?

  • What events should you raise?

In this section, I am going to present a methodology that can help answer these questions.

All programmers are familiar with using both procedural code and non-procedural resources when building their application. Windows resources that define dialogs, menus, and strings are a good example of non-procedural constructs. Have you ever built an application where you initialized an array of structures, then used the initialized array to drive your program? When designed properly, this technique allows us to change the behavior of our program by changing the data in the array. We don't need to touch the procedural code. This is another example of using a non-procedural construct.

Many programmers have used this non-procedural aspect of programming with Microsoft's tools. Visual Basic and Visual C++ have had this feature for years.

When performing systems analysis of software systems, there are many ways to slice and dice the application. We can create UML class diagrams. We can construct UML sequence diagrams. We can create data flow diagrams. We can document the entire system using use cases, either in text, or in diagrams. This division between procedural and non-procedural constructs is just one more way to view the design of the system that can yield insights. It can lead to applications that are easier to build and are more maintainable.

This technique doesn't take the place of classical object-oriented design. There are certain aspects of software systems that must be analyzed classically. It is an analysis technique to be used in conjunction with other design methodologies. This design methodology is, however, particularly germane to designing components and custom controls.

When we examine a problem with an eye towards analyzing what should be implemented non-procedurally, and what should be implemented procedurally, we need to pay attention to a particular idea. When we analyze, the question is what parts of the application have a non-procedural nature, and what parts have a procedural nature? If we implement something that is non-procedural in nature with procedural code, the procedural code tends to obfuscate the description of the non-procedural constructs. We have all seen a big, long section of code that creates a user interface. While we can read the code and discern what the user interface is, it is hardly obvious. Yet, when we look at a user interface in a design window, and see the properties of each of the controls in a property window, the user interface is quite easy to see.

It is interesting that Visual Studio .NET takes our non-procedural constructs (the properties of our components) and stores them in procedural code that is, by default, hidden from view. However, we don't need to concern ourselves too much with the method that the IDE uses to store the properties of components. It is more important that the way that we view and modify the properties is in an appropriate form, which is the tabular view in the Properties window.

What is procedural in nature should be described procedurally, and what is non-procedural in nature should be described non-procedurally. When our tool (Visual Studio .NET and the .NET Framework in this case) doesn't provide non-procedural constructs that match the abstractions in our application, then it is far better to first build components that implement the non-procedural constructs.

If we were to estimate the amount of non-procedural constructs as compared to procedural constructs, we might find that the typical programmer uses approximately 25 percent non-procedural constructs, and perhaps 75 percent procedural constructs:

Of course, you might have a different profile. The exact percentage doesn't really matter. What matters is that the further we push this line towards non-procedural code, the easier it is to build and maintain our application.

I have developed applications where greater than 80 percent of the application was specified in non-procedural constructs, and the results were great. Of course, before I developed the application, I needed to build the custom controls that implemented the non-procedural constructs, and there was plenty of procedural code within those custom controls.

Advantages of Non-Procedural Constructs

The main benefits of using non-procedural constructs are:

  • Better productivity- When developing a system, if we first build better abstractions using non-procedural constructs, we can then ‘assemble' the system. Changes in the specifications often don't require changes in procedural code.

  • Greater reliability- Once we debug a particular non-procedural construct, we can use that construct within the system with a minimum of debugging. Coverage testing is the process by which we verify that every procedural line of code has been executed, or if it hasn't been executed, we document why. We don't need to coverage-test the usage of non-procedural constructs.

  • Improved maintainability- It is far easier to document properties and the use of the properties than it is to document methods and their use. This makes it easier to transfer knowledge to maintenance programmers.

It is rare that an application designer can write the specifications for an application without making any mistakes. However, with properly constructed components and custom controls, in many cases, changes in the specification that happen further on in the development process can be easily accommodated, in many cases without changing any procedural code.

Often when building an application, it can be useful to build a component or custom control even when we are only planning on using a single instance of it. The reason is that this allows us to design a portion of our program that is data-driven. With a properly designed component, we can then change the behavior of our application by simply changing properties of the component. We don't need to touch the code.

Designing Components Using this Philosophy

So, how does this apply towards our process of making components and controls? There is a five-step process that we can take:

  1. We look at the application and determine the abstractions that are candidates for implementing using non-procedural constructs. In the case of user interfaces, do we have specialized validations that repeat throughout the application? Do we have repeating patterns of controls that occur on more than one window? We then document each of the abstractions, and what the abstractions do.

  2. Now that we know what the abstractions do, we ask the question, what do we need to know in order to do what we need to do?

  3. Then, we design properties that contain what we need to know.

  4. Next, we implement the controls with appropriate properties.

  5. Finally, we use them to assemble our application.

I tend to err on the side of making more properties than less. I have never been sorry that I defined more properties, even if I didn't ever use a particular property to change the behavior of an application. However, I have been sorry that I didn't abstract out a particular characteristic of a component and make it into a property.

If we make a property that we suspect may not be used, we can always configure the property so that it will not show up in the Properties window in Visual Studio. We will see how to do this when we discuss design-time configuration of components and custom controls in the next chapter.

A somewhat simplistic way to describe this philosophy is:

Make more custom controls, and give them more properties.

Another thought is that, if, to implement a particular piece of functionality, we have the choice of writing a method or creating some properties, the preference should be given to creating properties.

Don't write code to validate textboxes. Rather, design a custom control that has special properties (non-procedural constructs) that allow you to specify the validation in a non-procedural way. Don't write code to import data from or export data to a remote source. Rather, build a component that allows you to specify the import/export in a non-procedural way. After your components are mature, you will find that you will use these components over and over again without modifications in project after project.

Any time that you find yourself writing the same code over and over, this is a clue that there is an abstraction that you can generalize. More than this though, you should analyze your application and determine your abstractions ahead of time.

We can't specify everything in a non-procedural fashion. The very nature of what happens when the user presses a button is procedural. The procedures should be short and sweet though. Short event handlers are easier to debug than long event handlers.

Designing Events

When designing events, the events that we need to implement are normally fairly obvious. A Button control needs a Click event. A TextBox control needs an event that tells us when the text changes.

There is one area that developers should watch out for when designing events, and there are some guiding principles that should come into play when designing these types of events. They are:

  • Tell the module using the control what is about to happen, before it happens. In other words, have an event that indicates that the focus is about to move.

  • Give the module using the control some choice about what is about to happen. This is the concept of the refusable event. In the case of the .NET Framework, this is implemented by the CancelEventHandler delegate.

  • Then, we can take the action after the module using the control approves.

Not every event fits this model, and we might have a pair of events where the first event tells that something is about to happen, and is refusable. The second event tells that something already happened, and is not refusable. This is the case of the Validating and Validated events of the Control class.


Summary

In this chapter, we have progressively built more and more elaborate custom controls. From this foundation, we can build a large variety of types of controls.

We saw that custom controls are components that have a visual aspect. They are components that have additional responsibilities, such as drawing themselves, handling events, and generating events. We also saw that there are three main types of custom controls that we can build:

  • Built from scratch

  • Derived from an existing control

  • Assembled from multiple existing controls

We covered the mechanics of adding controls to the Toolbox. When building an application that uses the custom control, we covered the process of adding a reference to our custom control.

We saw the mechanics of adding properties to controls and also saw that there are two methods of informing Visual Studio .NET of the default values, either through an attribute that precedes the property, or by the addition of two methods that Visual Studio .NET will call to get the default value, and to reset the property to the default value.

Navigation is an important topic. Our custom controls should behave properly, and have the same user interface cues as native controls. We covered the mechanics of allowing a control to gain focus, and to have the correct focus cues.

We quickly reviewed delegates and events, and wrote a couple of controls that generate custom events.

Deriving custom controls from existing Windows Forms controls is a powerful technique to develop higher-level abstractions. This technique, used in combination with the technique of building composite custom controls, gives us a great deal of leverage.

There is more to the topic of developing custom controls. Specifically, we can enhance the design-time experience of the developer using our custom controls. In the next chapter, we will explore the design-time capabilities in depth.

We also discussed a particular philosophy of component design-that of declarative programming. We saw some of the benefits of making components and controls that are largely data driven. Using this philosophy can sometimes help us to decide what components and controls to make, and to design the properties and methods for them.

In the next chapter, we will move on to focus more on the design-time behavior of controls, showing you how to build controls that interact with Visual Studio .NET in a way that makes it very easy for other developers to use your controls.


Chapter 12: Design-Time Support

Overview

In this chapter, we are going to examine some of the ways in which you can make your controls integrate better with Visual Studio .NET, in turn making it easier for other developers to use your controls in their code. In particular we will show how to:

  • Enhance the way the properties of your control are edited in the Properties window

  • Allow for Visual Studio .NET to bring up a modal dialog box or drop-down control to edit values of properties in your control

  • Enhance the way that your control is displayed in the Design view itself

This means, incidentally, that none of the code we develop in this chapter will have any effect on how a custom control behaves at run-time. Rather, it will improve the way that others are able to use your control at design-time, when they are building it into some other application. The concepts that we will develop are nevertheless extremely important. As a developer used to using Visual Studio .NET, you will be accustomed to having high expectations of the ease with which you can manipulate controls that you place in your forms. Well, other developers will have similar expectations when it comes to manipulating the controls that you write. In this chapter, we will show you how to satisfy those expectations.


Basic Concepts

With most previous component technologies, some of the enhanced design-time support was embedded in the design tool rather than the component. This meant that while we might have had a very complex custom control, we had a limited ability to affect how the design tool visually represented the control. Only certain controls, where the enhanced design-time support was built into the IDE, had a graphical representation that was representative of the actual control, or allowed direct manipulation of its characteristics.

All of this has changed with the .NET Framework and Visual Studio .NET. Now, all aspects of the design-time representation of the control are embedded in the control, not the development environment. This is true for all controls, including the controls in the Windows Forms namespace. We can make the design-time representation as elaborate as we wish to.

We will now review in more detail the areas in which we can add greater integration with Visual Studio .NET. These areas really come down to two broad topics:

  • Improving the way properties are manipulated in the Properties window

  • Improving the way a control as a whole is displayed in the Design view

Editing Properties in the Properties Window

In most cases, when we build custom components and controls, we can use them in an Integrated Development Environment (IDE) such as Visual Studio .NET. We can add the controls to the Toolbox window. We can drag them from the Toolbox to the Design window for a form. We can select the control on the form and edit its properties.

However, if one of our properties is a class or structure, unless certain provisions are made, we can't edit the property. At the very least, there needs to be provision for creating a string from the structure, and creating the structure from a string. For example, if we declare a property of type Point, we can edit the property as a string by entering the two components of the Point (width and height), separated by a comma. In addition, with the Point structure, we can treat it as a composite property, and expand the property and edit the width and height separately.

In addition, there are a number of enhancements that we can make so that editing complex properties is much more convenient. We may design a custom graphical user interface for our property, so that the developer using the property can use some form of direct manipulation to edit the property. We may put up a modal dialog box with a number of fields, checkboxes, radio buttons, and so on, with which we can edit a complex property in a more intuitive form.

The following is a list of the ways in which we can enhance design-time support for properties, from the simplest to the most elaborate:

  • Assigning default values to properties

  • Hiding properties that aren't relevant at design-time so they don't appear in the Properties window at all

  • Assigning properties to categories

  • Converting properties to and from strings using a TypeConverter

  • Creating composite properties that we can expand and collapse using a TypeConverter

  • Creating a modal dialog box or a drop-down window in which we can edit a property

  • Creating a drop-down window in which we can edit a property

We have actually already seen how to do the first two points in this list in the last chapter, when we introduced the DefaultValue and Browsable attributes. Let's quickly go over the rest of the list.

Categorizing Properties

When we define a property, we can also define a category for it. When we open the Properties window in Visual Studio .NET, we have two viewing options. We can see the properties organized by category, or we can see the properties sorted in alphabetical order. We switch between these two options via the toolbar at the top of the Properties window.

The following screenshot shows properties sorted by category:

Click To expand

We can collapse and expand categories in the Properties window. The Layout category above is collapsed.

Expanding Composite Properties and Converting to Strings

Adding one more level to a hierarchical view in the Properties window, we can design composite properties that expand and collapse, but which can also be edited directly via string representations. This allows us to further organize properties for ease of editing. In the next screenshot, the StartingDisplayPoint property is a composite property that expands. Hence, the developer can either edit the X and Y components of it independently or can edit the string representation-in this screenshot 6,6:

Click To expand

We will learn how to create properties such as StartingDisplayPoint in the DesignNumberTextBox sample later in this chapter.

Custom Property Editors

There are a wide variety of custom property editors that are already defined for certain types within the .NET Framework:

  • Font types are edited with a Font selection dialog box

  • Colors are edited with a color picker dialog box

  • The Anchor property allows us to interactively select how a control is anchored to the sides of its containing control

We should also point out that bool and enum properties are edited with a drop-down list, which can be regarded as similar to a custom property editor.

These custom editors can either be put in the ‘drop-down' position, or they can be opened as modal dialog boxes.

Editing the Control in the Design View

For complex custom controls, we can enhance the way that the control is displayed on the design surface. For example, if we were to build a complex spreadsheet control, we might change the representation so that the developer using the control could change widths of columns through direct manipulation with the mouse. Another example; some controls, while they take up screen real estate, may not, by default, have any appearance. A control that groups other controls together is an example of this. In this case, we will want to at least draw a dotted line that represents the control so that the developer using the control can manipulate it.

We will cover this aspect of improving controls towards the end of the chapter when we develop the SimpleBlankControl and SimpleLineControl examples.


Categorizing Properties and Events

In the last chapter, when we added properties and events to our custom controls, by default the properties and events were added to the Misc category.

We have already seen in the last chapter a couple of attributes that affect how Visual Studio .NET displays properties in the Properties window:

  • Browsable-Used to prevent properties from appearing at all

  • DefaultValue-Causes values to display in bold if they are not the default value

In this section, we will introduce a third attribute, Category. The Category attribute will cause the property to be placed under the given named category.

As an example, consider the CustomControlDefaultPropValues example from the last chapter. Our control in that example had several properties, such as the DisplayText and DisplayCount, all of which are really related to the control's appearance and ought, therefore, to be in the Appearance category. We can do this by adding the Category attribute. The code shown here is for the DisplayText attribute. The code is identical for the other attributes:

[DefaultValue("Hello, World"), Category("Appearance")]
public string DisplayText
{
  get
  {
    return displayText;
  }
  set
  {
    displayText = value;
    Invalidate();
  }
}

In C#, the code can alternatively be written as follows:

[DefaultValue("Hello, World")]
[Category("Appearance")]
public string DisplayText
{
     .      .
     .      .
     .      .

Now, when we open the Properties window, we see the attributes in the Appearance category:

Click To expand

Within each category, the properties are sorted by property name (although you can change this by adding the ParenthesizePropertyName attribute, which forces the property to go to the top of the category, besides putting its name in parentheses).

Note that in this example, the properties all had another category, DefaultValue, already applied. This is not necessary-the two attributes are independent so you can use the Category attribute by itself. However, good programming practice with controls would suggest you ought to supply both a DefaultValue and a Category to all browsable controls, to make it easier for developers using them.

Using the Category attribute, you can place each property in whatever category you wish. You can either use one of the existing categories (such as Appearance) or make up your own simply through your choice of the string you supply to the Category attribute.


Improved Editing in the Properties Window

We are now going to return to the NumberTextBox custom control from the last chapter in order to add improved design-time support to it. In this section, we will focus on how the properties of the control are edited in the Properties window.

In the last chapter, we had the possible range of numbers entered as a string, for example, 0,100 in order to allow the developer (or any client code) to modify both values at the same time. What would really be nice though, is to allow the values to be edited together or individually. That's what we are going to achieve. The Visual Studio .NET solution we will develop in this section will look and behave exactly the same as the NumberTextBox example. However, it will have enhanced editing facilities in the Properties window, as shown in this screenshot:

Click To expand

This screenshot shows that the NumberRange property is expandable so that you can edit it, either by typing in the comma-separated values as a string, or by typing in the individual Low and High components.

However, our example will demonstrate something else at the same time. Up until now, we have only been able to edit properties that are of certain datatypes in the Properties window, for example, a string or point. If one of our custom controls implemented a property that was of a type we had defined, then Visual Studio .NET will not, by default, allow you to edit that property in the Properties window. This is because the IDE has no way of knowing what your intentions might be regarding how a developer should be able to edit that type. With the techniques we develop in this section, we will be able to do that. In fact, in this example, NumberRange is an instance of a struct that we will define, also called NumberRange-yet as the above screenshot shows, Visual Studio .NET is happily able to allow full editing functionality for the possible values of this struct. As we will see when we come to code up the sample, doing this requires two players:

  • The struct (or class) that represents the property

  • A type-converter class that is derived from System.ComponentModel.TypeConverter and that controls conversion of the property type to and from a known type, such as string

To allow developers using our control to edit this new property, we need to create a TypeConverter for the class. The TypeConverter class does two things for us. It allows the developers to edit the property as a string. The way that the developer edits the string will be the same as in the previous versions of this custom control, in other words, the low and high parameters are viewed and entered as a comma-separated string. However, our parameters will be stored in our own class. The TypeConverter also enables our property to be a ‘composite' property, that we can expand and collapse in the Properties window.

Both functions that the TypeConverter serves are important. Some developers will prefer to edit the property as a string, rather than expanding the property, and even if a developer prefers to expand the property to edit it, the conversion to a string data type is important because the conversion to string is used to provide the value of the property in the right column of the Properties window when the property is not expanded.

This custom control will not have much code in common with the previous version of NumberTextBox, so let's start a new project from scratch.

Create a new Windows Control Library project, and name it DesignNumberTextBox. For this example we will use the name DesignNumberTextBox for the solution and project, but the control itself will be named NumberTextBoxA to distinguish it from the NumberTextBox class of the last chapter. So, if you are creating this project as you read, you should make the appropriate changes to your C# files. (Or as always, you could simply download the complete sample from the Wrox Press web site at http://www.wrox.com).

We will now introduce the new struct, which will perform data validation.

The NumberRange Struct

This is the struct that holds the low and high limits and performs the data validation. This structure replaces the string that we used in the previous version of the NumberTextBox. In a real-world application, I would make this structure quite elaborate, with many types of data validations in it. I would have an enumeration for all the types of validations, and a number of parameters to be used for each type of validation. For our example though, the validation is a simple test of whether a given number lies within the specified range.

The definition of the NumberRange struct starts off like this. We define a couple of member fields that define the low and high limits of the range, along with corresponding properties:

public struct NumberRange
{
  private double low;
  private double high;

  public NumberRange(double low, double high)
  {
    this.low = low;
    this.high = high;
  }

  public double Low
  {
    get
    {
      return low;
    }
    set
    {
      low = value;
    }
  }

  public double High
  {
    get
    {
      return high;
    }
    set
    {
      high = value;
    }
  }

We also said this struct will be responsible for validating data, so we also need to type in this method:

  public bool Validate(string s)
  {
    double val;
    // Check value is a number,
    try
    {
      val = Double.Parse(s);
    }
    catch (FormatException)
    {
      return false;
    }

    // If high < low then check number is OUT of range.
    if (high < low)
    {
      if (val < low && val > high)
        return false;
      else
        return true;
    }
    // If high >= low then check number is IN range.
    if (val < low | | val > high)
      return false;
    return true;
  }
}

This method does exactly the same thing as the ContentsInRange property of the Number Text Box class in the sample from the previous chapter, except that due to the need to pass in the string as a parameter, it is now a method rather than a property. Recall also from the last chapter that if the high limit is lower than the low limit, we interpret this as a requirement that the number should lie outside rather than inside the specified range.

At this point, we have a complete struct to perform data validation. What we don't yet have is any ability for Visual Studio .NET to allow editing of the values of this type, which is after all the whole point of this sample. This aspect comes by adding the following attribute to the struct:

[TypeConverter(typeof(NumberRangeConverter))]
public struct NumberRange
{
       .      .
       .      .
       .      .

Up until now, we haven't covered much new territory. Now comes the fun part.

The TypeConverter-Derived Class

The next class is the NumberRangeConverter class. This class derives from the TypeConverter class, which implements four main areas of functionality:

  • Type conversion between our property data type and the string data type

  • Type conversion from our property data type to an InstanceDescriptor object

  • Sub-properties, so that the developer using the control can expand/contract the property

  • Re-creation of the data type when the data type is immutable

The first area of functionality is type conversion between our NumberRange structure and a string. We convert to and from the string data type so that the Properties window can display a string in the right column for the composite property itself. Conversion to a string allows display of the property, while conversion from a string allows developers to edit the composite property, perhaps as a comma-delimited string, as implemented in this example.

The second area of functionality of the NumberRangeConverter class is to convert from our property data type to an instance of the InstanceDescriptor class. Visual Studio .NET uses an InstanceDescriptor object for generation of code for modifying the NumberRange property. The InstanceDescriptor object describes the constructor signature of the NumberRange class, and from the constructor signature, the code generator can write instantiation code in C#. Note that we only need to do a type conversion in one direction-from NumberRange to InstanceDescriptor.

Although these areas of functionality have different purposes, they are implemented in the same methods, so we will consider them together as far as the code is concerned.

Type Conversion Methods

There are four methods that we must implement to effect type conversion:

  • CanConvertFrom()-Test against the Type argument, and return true or false based on the type.

  • CanConvertTo()-Test against the Type argument, and return true or false based on the type.

  • ConvertFrom()-Convert the value argument (of type object), and return an object of our parameter type (NumberRange in this case).

  • ConvertTo()-Convert the value argument, which is declared as type object, but is actually of type NumberRange, and return an object of the destination type, which will be either of type string or type InstanceDescriptor. It will be only one of these two types because these are the two types to which we agreed to convert in the CanConvertTo() method.

These methods are all defined in the System.ComponentModel.TypeConvertor class, and we need to override these methods to provide our own type conversion ability.

All four of these methods take an ITypeDescriptorContext as an argument. From this argument, we can determine the actual control that instigated the type conversion request, and we can determine the container of that control, just in case we need that information in order to perform the conversion. In our example, we don't care what the context is; our conversion is not affected by it.

In addition, the CanConvertTo() and ConvertTo() methods take an argument of type CultureInfo. We could use this information to influence the conversion. As an example, we could use this argument to modify the conversion of a decimal to a string, so that the conversion happened correctly based on the CultureInfo. In our example, we don't care about this argument.

Let's start with the class definition and the CanConvertFrom() method. All this method needs to do is return a value to indicate that our class will allow conversion from a string, as well as any conversions supported by the base class:

public class NumberRangeConverter : System.ComponentModel.TypeConverter
{
  public override bool CanConvertFrom(
    ITypeDescriptorContext context, Type sourceType)
  {
    if (sourceType == typeof(string))
      return true;
    return base.CanConvertFrom(context, sourceType);
  }

Next is the method that actually does the conversion from a string. The code here is very similar to that in the last chapter for the NumberTextBox sample. We simply identify where the comma is in the string, and assume that the substrings separated by the comma are the low and high values:

public override object ConvertFrom(
  ITypeDescriptorContext context, CultureInfo culture, object value)
{
  if (value is string)
  {
    string[] v = ((string)value).Split(new char[] {','});
    if (v.Length != 2)
      throw new ArgumentException(
        "number range string must be of form <low limit>,<high limit>");
    NumberRange nr =
      new NumberRange(double.Parse(v[0]), double.Parse(v[1]));
    return nr;
  }
  return base.ConvertFrom(context, culture, value);
}

Now we have to do the same thing, but converting to a value. The CanConvertTo() method needs to return a value to indicate we can convert to either a string or an instance descriptor, or to any type supported by the base class:

public override bool CanConvertTo(
  ITypeDescriptorContext context, Type destinationType)
{
  if (destinationType == typeof(string))
    return true;
  if (destinationType == typeof(InstanceDescriptor))
    return true;
  return base.CanConvertTo(context, destinationType);
}

And similarly, having informed Visual Studio .NET that we can do these conversions, we need to implement the method to actually perform the conversions:

public override object ConvertTo(
  ITypeDescriptorContext context, CultureInfo culture, object value,
    Type destinationType)
{
  if (destinationType == typeof(string))
  {
    NumberRange nr = (NumberRange)value;
    return nr.Low + "," + nr.High;
  }
  if (destinationType == typeof(InstanceDescriptor))
  {
    NumberRange nr = (NumberRange)value;

    // Specify that we should use the two-parameter constructor.
    return new InstanceDescriptor
      (typeof(NumberRange).GetConstructor(
      new Type[2] {
        typeof(double),
        typeof(double)
      }),
      new double[2] {
        nr.Low,
        nr.High
      });
  }
  return base.ConvertTo(context, culture, value, destinationType);
}

The code for conversion to a string simply sticks the low and high values together in comma-delimited format. The conversion to an instance descriptor supplies information about the constructor of the type used for the property. Recall that NumberRange has a constructor that takes two double parameters, used to populate the Low and High properties:

public NumberRange(double low, double high)
{
      .      .
      .      .
      .      .

The above code returns an InstanceDescriptor that contains that information in a format that Visual Studio .NET can understand. We don't have space here to go further into details of the InstanceDescriptor class, but you can find the information in the MSDN documentation if you want to explore further.

Sub-Properties

The third area of functionality for a type converter is implementation of sub-properties. To implement this, we override two methods, GetPropertiesSupported(), and GetProperties(). Our override of the GetPropertiesSupported() method simply returns true, to indicate that this custom class has properties. Our override of GetProperties() needs to return an object of type PropertyDescriptorCollection. There is a convenient method of the TypeDescriptor class that returns a PropertyDescriptorCollection if we pass in a type. This method uses reflection to get a list of all of the properties in our NumberRange data type. If we want to change the list of sub-properties, we can modify the PropertyDescriptorCollection before we return it.

Here are the two overridden methods:

public override bool GetPropertiesSupported(
  ITypeDescriptorContext context)
{
  return true;
}
public override PropertyDescriptorCollection GetProperties(
  ITypeDescriptorContext context, object value, Attribute[] attributes)
{
  PropertyDescriptorCollection properties =
    TypeDescriptor.GetProperties(typeof(NumberRange), attributes);
  return properties;
}

The GetProperties() method takes an ITypeDescriptorContext as an argument. We could use this to determine the actual object that instigated the request.

In addition, GetProperties() takes an array of Attribute objects, to be used for filtering. The main purpose for this argument is to filter out all properties for which the Browsable attribute is set to false. As it turns out, there is a GetProperties() method in the TypeDescriptor class that also takes an array of attributes, for exactly the same purpose. We can pass the Attribute array to the method, and it takes care of the issue for us.

Dealing with Immutable Classes

In this section, we will take a quick break from the sample to comment on the situation if the type representing our property is immutable. Recall that an object of an immutable class (such as System.String) can never be modified once it has been created. This means that if you are editing the property values of an immutable class in the Properties window, Visual Studio .NET won't be able to modify the object-it will have to delete that object and create a new one with the new values. Our NumberRange class is not immutable so those considerations don't apply to it, but we will take a quick look here at the type of code you would need to put in the type converter class if you were dealing with an immutable type. The code in this section does not form part of the DesignNumberTextBox example.

If we are to enable Visual Studio .NET to destroy and rebuild the object that contains the value of an immutable composite property, then we need to implement two methods in the type converter class to enable this functionality-GetCreateInstanceSupported() and CreateInstance().

GetCreateInstanceSupported() should return a bool that indicates whether the type converter class can supply information about how to create a new instance of the property. We override this method to return true.

CreateInstance() actually returns the information needed to construct a property instance. We override this method and implement it as follows. This code describes the situation for the NumberRange class, whose constructor takes two parameters that are used to set the Low and High properties:

public override object CreateInstance(ITypeDescriptorContext
  context, IDictionary propertyValues)
{
  return new NumberRange((double)propertyValues["Low"],
    (double)propertyValues["High"]);
}

We can use the indexer of the IDictionary interface, passing in the names of our properties, then cast the properties to the correct type, then call the constructor of our NumberRange structure, and then return the newly created object.

We shouldn't, however, implement this functionality unless our data type is immutable.

The NumberTextBoxA Control

We will now continue with the DesignNumberTextBox sample. The next class to examine is the NumberTextBoxA class, which derives from System.Windows.Forms.TextBox, and which represents the actual control we are really developing. The class is similar to the NumberTextBox class from the previous chapter. The differences between this class and NumberTextBox are solely because the validation is now controlled by the new NumberRange class. Nevertheless, there are enough differences that we'll still go through the code for this class in detail.

We start off by declaring a member field to hold the NumberRange validator. We also define a static, read-only field that holds the default value for NumberRange. Having this static field means that if we decide to change the default value we can do so easily:

public class NumberTextBoxA : System.Windows.Forms.TextBox
{
  private static readonly NumberRange defaultNumberRange =
    new NumberRange(0,100);
  private NumberRange numberRange = defaultNumberRange;

Following the private member variables are the properties themselves. These properties are similar to the properties that we have used previously in custom controls. One minor difference is that the NumberRange property returns a structure, as opposed to a variable such as a string or integer. We have also defined a suitable category for this property now that we know how to do that:

[Category("Validation")]
public NumberRange NumberRange
{
  get
  {
    return numberRange;
  }
  set
  {
    numberRange = value;
  }
}

Next, we need to deal with informing Visual Studio .NET of the default value of the NumberRange. Since NumberRange is now a struct we have defined, rather than a string, we can no longer use the Default Value attribute to do this, so we will have to use the Reset/ShouldSerialize technique:

public void ResetNumberRange()
{
  NumberRange = defaultNumber Range;
}

public bool ShouldSerializeNumberRange()
{
  return ! NumberRange.Equals(defaultNumberRange);
}

Although validation is performed by the NumberRange class, we still want our control to expose a property indicating if the contents are valid. This is it:

[Browsable(false)]
public bool ContentsInRange
{
  get
  {
    return this.NumberRange.Validate(this.Text);
  }
}

Just as in the earlier sample, we also have a property that returns a suitable validation error message:

[Browsable(false)]
public string ValidationErrorMessage
{
  get
  {
    if (ContentsInRange)
      return "";
    if (numberRange.High >= NumberRange.Low)
      return "Value must lie between " + NumberRange.Low.ToString() +
        " and " + NumberRange.High.ToString();
    else
      return "Value must NOT lie between " + NumberRange.High.ToString()
        + " and " + NumberRange.Low.ToString();
  }
}

The final bit of code for this class is the event handler for the Validating event, which changes the background color of the control to yellow if it detects invalid contents. The code for this method is identical to that in the earlier sample, and just as before, you should make sure this method is attached to the Validating event:

private void NumberTextBoxA_Validating(
  object sender, System.ComponentModel.CancelEventArgs e)
{
  if (ContentsInRange)
  {
    this.BackColor = Color.White;
  }
  else
  {
    this.BackColor = Color.Yellow;
  }
  e.Cancel = false;
}

This almost completes the code for the NumberTextBoxA class, except that we need to add two using statements to the top of our class:

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using System.Globalization;
using System.ComponentModel.Design.Serialization;

Now we can build the control and add it to the Toolbox if it is not already added.

Creating a Test Application

We won't go through the details of creating the test form for this project in detail, since this part is virtually identical to the test application we used in the previous chapter for the NumberTestBox project. The only difference is that this time we need to place NumberTextBoxA controls instead of NumberTextBox controls on the form. And when you do so, you will find that you can edit our composite property in the Properties window either with the string or by expanding the property and editing the sub-properties.


Debugging Design-Time Code

Parts of our code in the previous sample are operational at design-time. Some of these parts are:

  • Any drawing code of our custom control will execute when our control is drawn in the design window

  • The constructor of our custom control will execute when our control is built by Visual Studio .NET

  • When any properties are set, the code in our properties executes

What we mean by the code being operational at design-time is that these sections of code actually execute as we are manipulating our control in the Design window of Visual Studio .NET, and as we are editing properties in the Properties window. We haven't run a program that is using our control-we only have placed our control in the design window. If we have a bug in our code, perhaps in the TypeConverter, or in the code to set or get a property value, we can get some very odd behavior from Visual Studio .NET. If we write code that causes an error message to be shown, and dismissing the error message causes the error message to be shown again, or we have a Paint event loop, we can get into a situation where we have no option other than to use the task manager to kill the Visual Studio .NET process. We would then need to restart Visual Studio .NET, and fix our code.

This presents a bit of a problem for debugging. We can't debug this code in the Visual Studio .NET debugger. Visual Studio .NET is not running our application yet. We can't write to the Console, as it is not available while Visual Studio .NET is in design mode. In Visual Studio .NET, we could open the Console window, but any text that we write to it will not be visible unless we are running the application. The solution that worked best for me was to write debugging statements to a text file. At each point where I wanted to insert a debugging statement, I would open the file; append text to the file, then close the file:

System.IO.StreamWriter sw = new System.IO.StreamWriter("debug", true);
sw.WriteLine("ConvertTo: creating NumberRange");
sw.Close();

In a separate text editor, I could examine the file.


Creating a Modal Dialog Box Property Editor

Some properties may not lend themselves to editing via the standard property editor. There are several examples of these properties in the .NET Framework:

  • When setting a font for a control, Visual Studio .NET brings up the Font Selection Dialog. This is a modal dialog that is centered in the screen.

  • When setting a Color property, Visual Studio .NET brings up a color selection window. This window doesn't have any border decorations, such as a close box, the ability to resize the window, and so on. This window is created in the drop-down position.

  • When setting the Anchor property for a control, Visual Studio .NET brings up a small window that lets us visually indicate where the control is anchored.

We will now make a new version of our NumberTextBox custom control that allows us to edit the NumberRange in a modal dialog instead of editing the values in sub-properties.

Create a new Windows Control Library project and name it DesignDlgNumberTextBox. Then, rename UserControl1.cs to NumberTextBoxB.cs.

In this example, we will need four classes:

  • Our custom control-This is the class that derives from the TextBox class. The code for the class is virtually the same as for the last example. Since we are using different namespaces, we could theoretically use the same name for this class, but we will instead name it NumberTextBoxB. The sole reason for this name change is that if you download and run both samples from the Wrox Press web site, you might otherwise end up with two controls in your Visual Studio .NET Toolbox with the same class name! Despite the different namespaces, this could still be confusing!

  • The class that performs data validation-This is identical to the NumberRange struct from the last sample. We will retain the name NumberRange for this struct, and won't consider this class further.

  • A Windows Form dialog called NumberRange Dialog-This is the class that represents the dialog that will be used within Visual Studio .NET to edit NumberRange values.

  • Our custom property editor-This class derives from UITypeEditor, and is a class that tells Visual Studio .NET how to bring up the NumberRangeDialog dialog box to edit the property.

Besides these four classes, we will also need a test form to run the application. This form is identical to the previous test forms, apart from containing instances of NumberTextBoxB controls instead of NumberTextBoxA.

The NumberTextBoxB Control

The only difference between this NumberTextBoxB.cs and the previous version is the addition of another attribute before the declaration of the NumberRange structure. Modify the attributes as follows:

namespace Wrox.GDIPlus6314.Ch12.DesignDlgNumberTextBox
{
  [Editor(typeof(NumberRangeEditor), typeof(UITypeEditor))]
  [TypeConverter(typeof(NumberRangeConverter))]
  public struct NumberRange
  {
      .      .
      .      .
      .      .

What we have added is an Editor attribute. This attribute tells Visual Studio .NET (or any other application that wants to know for that matter) which class is used to edit instances of this property. There are several overloads of this attribute, but the one we use takes two parameters, which should both indicate types. The first parameter is the class that Visual Studio .NET can use to control the editing of the property. This class must inherit directly or indirectly from the .NET Framework class UITypeEditor. The second parameter is the base class of your class. For most purposes this parameter seems redundant-it will normally be UITypeEditor, but there may be specialist cases for which you will want to supply a derived class. We won't consider that situation in this book.

Note that UITypeEditor is defined in the System.Drawing.Design namespace, so we also need a reference to this namespace:

using System.Drawing.Design;

Next, we need to consider the classes involved with bringing up the dialog box. We will look at the dialog class itself before considering the NumberRangeEditor class that actually links the dialog to the NumberRange property.

The NumberRangeDialog

We will now create the dialog responsible for editing the NumberRange control. The dialog, when running, looks like this:

Click To expand

This dialog looks quite similar to the actual form that comes up when the program is running, but don't be fooled by the similarity-it is not the same dialog. This dialog pops up at design-time, not program run-time, and those are TextBoxes on it, not NumberTextBoxes (actually, it would be possible to use NumberTextBoxes, which would leave us in the ironic situation of having a class used to edit instances of itself, but we won't do that here).

To create this dialog, create a new Windows Form by right-clicking on the project in the Solution Explorer and selecting Add | Add Windows Form. Set the name of the file to NumberRangeDialog.cs, and use the Toolbox to add the two TextBoxes, two Buttons, and two Labels as shown above.

Now set the following properties for the TextBox controls:

Property | textBox1 | textBox2
Text |  |
(Name) | tbLow | tbHigh

and then for the Button controls:

Property | button1 | button2
Text | &OK | Cancel
(Name) | btnOK | btnCancel
DialogResult | OK | Cancel

Select the form by clicking on it somewhere, but not on any of the controls and set the following properties:

Property | Value
Text |
FormBorderStyle | FixedDialog
AcceptButton | btnOK
CancelButton | btnCancel

Now we need to modify the code for the form so that it saves the typed in values for the NumberRange if the user clicked the OK button (or hit Enter).

Add a private member variable to the NumberRangeDialog class:

public class NumberRangeDialog : System.Windows.Forms.Form
{
  private NumberRange editedNR;

This field will represent the NumberRange instance that the dialog is editing.

Add the a corresponding public property for this field:

public NumberRange EditedNumberRange
{
  get
  {
    return editedNR;
  }
  set
  {
    editedNR = value;
  }
}

Modify the constructor of the NumberRangeDialog class to take a NumberRange structure as an argument-this will provide the initial value of the editedNR field:

public NumberRangeDialog(NumberRange editedNR)
{
  editedNR = editedNR;
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent();
}

Double-click on the form (but not on a control) to add a Load event handler for the form. Modify the Load event handler as follows:

private void NumberRangeDialog_Load(object sender, System.EventArgs e)
{
  tbLow.Text = editedNR.Low.ToString();
  tbHigh.Text = editedNR.High.ToString();
}

This Load event handler ensures that the two textboxes are initialized correctly.

Next we need to add Validating events for the two textboxes:

private void tbHigh_Validating(
  object sender, System.ComponentModel.CancelEventArgs e)
{
  try
  {
    Double.Parse(tbHigh.Text);
  }
  catch (FormatException)
  {
    MessageBox.Show("Invalid value", "Validation Error",
      MessageBoxButtons.OK, MessageBoxIcon.Error);
    e.Cancel = true;
    return;
  }
}

These event handlers simply check that the user has typed in numeric values. If they haven't, then the handlers display dialog boxes. This isn't a problem-the dialog boxes will be displayed within the Visual Studio .NET environment.

Finally, double-click on the OK button to add a Click event that can store the values the user has typed in:

private void btnOK_Click(object sender, System.EventArgs e)
{
  editedNR.High = Double.Parse(tbHigh.Text);
  editedNR.Low = Double.Parse(tbLow.Text);
}

This code ensures that when the user OK's the dialog, the new values for the NumberRange that they typed in are stored and can be later accessed using the NumberRangeDialog.EditedNumberRange property.

As we will see soon, the NumberRangeEditor class will retrieve the value of this property so that it can inform Visual Studio.NET of the new value.

The NumberRangeEditor

As indicated earlier, this is the class that Visual Studio .NET actually calls up in order to get the editing of the NumberRange done. This is because we told Visual Studio .NET about this class through the Editor attribute.

Before we examine the code for this class, let's examine in principle what Visual Studio .NET is expecting our code to do. In order to instantiate the dialog box, the IDE is going to invoke a number of virtual methods defined in the UITypeEditor class, which we will need to understand. It is important to understand that Visual Studio .NET never really communicates directly with our NumberRangeDialog class-it doesn't know how to because the definition of that class is entirely up to us. Instead, Visual Studio .NET talks to our UITypeEditor-derived class, NumberRangeEditor, and asks it to arrange everything. So, our NumberRangeEditor must be able to talk to Visual Studio .NET on the one hand, which it does by implementing certain UITypeEditor methods that the IDE knows about, and to our dialog on the other hand, which it clearly does in the implementations of those methods that we will write.

The methods we need to override are:

Method | Purpose
EditValue() | Gets called when the developer clicks the button to call up the editor for a property. This method is passed the current value of the property and must display the dialog box, obtain any new values from the dialog, and return the new value of the property, cast to object.
GetEditorEditStyle() | Returns an enum value indicating the style of the dialog (or drop-down list) that is to be used.

Another point that it is important to grasp is that when we drop a control onto the design window in Visual Studio .NET, the IDE creates an instance of our custom control. This object draws itself in the Design window. It also contains the values of the properties that we set in the Properties window. In addition, when we build a custom property editor, this instance of our custom control is passed as an argument to the EditValue() method, in the value parameter. One of the first things that this method does is create a variable of the type of our custom control, and assign the value parameter (while casting) to the object. We will need this custom control for a variety of reasons.

So, we need to create a class that derives from UITypeEditor. This class is a simple C# class, not a control, so you can just create a new class to the project, call it NumberRangeEditor.cs, and then edit the base class. We will also need a few namespace references:

using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace Wrox.GDIPlus6314.Ch12.DesignDlgNumberTextBox
{
  public class NumberRangeEditor : System.Drawing.Design.UITypeEditor
  {

First, we will define the UITypeEditorStyle() method:

public override UITypeEditorEdit Style GetEditStyle(
  ITypeDescriptorContext context)
{
  if (context != null && context.Instance != null)
  {
    return UITypeEditorEditStyle.Modal;
  }
  return base.GetEditStyle(context);
}

There is one other method that we must implement in this class-the GetEditStyle() method. As we have indicated before, there are two styles of property editors. The style that we are building right now is a modal dialog box style. All this method does is return UITypeEditorEditStyle.Modal. In the next example, where we implement a drop-down style of property editor, we will change this method appropriately.

This method gets passed an interface reference to the interface System.ComponentModel.ITypeDescriptiorContext. We won't worry about this interface here, since, beyond checking that it isn't null, we don't need it. It can be used to obtain information about the property that we are editing, should our method require it (if for example, we are using the same editor class to control the editing of different properties of different types).

Now let's check out that edit method:

public override object EditValue(
ITypeDescriptorContext context, IServiceProvider provider, object value)
{
  IWindowsFormsEditorService edSvc = null;

  if (context != null
    && context.Instance != null
    && provider != null)
  {

    edSvc = (IWindowsFormsEditorService)
      provider.GetService(typeof(IWindowsFormsEditorService));

    if (edSvc != null)
    {
      NumberTextBoxB originalControl = (NumberTextBoxB)context.Instance;
      NumberRangeDialog editingDialog =
        new NumberRangeDialog (originalControl.NumberRange);

      DialogResult dr = edSvc.ShowDialog(editingDialog);

      // This method could take different actions based on
      // the return value of the dialog,
      if (dr == DialogResult.OK)
      {
        value = editingDialog.EditedNumberRange;
        return value;
      }
    }
  }
  return value;
}

This method also takes an ITypeDescriptorContext as a parameter, and this time, we are going to need to use it. The EditValue() method also takes a second parameter, a reference to a System.IServiceProvider interface. IServiceProvider defines one method, GetService(), which returns a reference to the object inside Visual Studio .NET that should call up the dialog, cast to object, as well as a third parameter that takes the return value of the property, if any.

Let's look at that second parameter in more detail. Since we are building a property editor, Visual Studio .NET needs to do the actual opening of the dialog. From the IServiceProvider argument we can get an instance of a class that implements the IWindowsFormsEditorService interface. This interface defines a method, ShowDialog(), which we call when we want to put up our dialog. That way, it is still Visual Studio .NET that controls the actual showing of the dialog. It sounds more complicated than it actually is. We just follow the recipe as seen in this example:

edSvc = (IWindowsFormsEditorService)
  provider.GetService(typeof(IWindowsFormsEditorService));

followed later by:

DialogResult dr = edSvc.ShowDialog(editingDialog);

We use the context argument to obtain the original value of the Number Range property, so we can pass it to the dialog:

NumberTextBoxB originalControl = (NumberTextBoxB)context.Instance;
NumberRangeDialog editingDialog =
  new NumberRangeDialog (originalControl.NumberRange);
DialogResult dr = edSvc.ShowDialog(editingDialog);

Once the call to ShowDialog() has returned, we simply return the result if necessary.

We are finally done with our custom control. Compile it and add it to the Toolbox.

Now we need to build a test application.

Creating a Test Application

Add a new Windows Application to the project and name it TestDesignDlgNumberTextBox. Set it as the startup project and drag two NumberTextBoxB controls onto the form.

Now, we can edit the NumberRange property three ways:

  • We can edit it as a comma-delimited string

  • We can expand the property and edit sub-properties

  • We can click the button to the right of the value of the property, and edit the parameters in our dialog box

This is a very simple example, but we could build a much more elaborate dialog box for editing larger data types.


Creating a Drop-Down Property Editor

A different style of property editor is one that ‘drops down' below the property value. This type of property editor provides a sense of context to the developer using the custom control-since the window in which they edit the property is directly below the property, there is a visual link between the two. In addition, there is a slight user interface variation. A drop-down property editor is a window that automatically closes when the developer using the control clicks outside of the property-editing window.

To create this type of property editor, there is a basic difference in the way we code it. Instead of creating a dialog box, we create a custom control to use in the UITypeEditor. If we are creating a modal dialog box property editor, we must create a dialog box to use. If we are creating a drop-down property editor, we must use a custom control.

There is the potential for confusion here-we now have two custom controls. We will name this new custom control NumberRangeDropDown. To make things clear, in this section, whenever I refer to a custom control, I will identify it by name.

Just as in the last example, we will need four classes in our DLL project, plus a Windows Form class to test the application with. Most of these classes are, however, very similar to the old project, and we will give some of them the same class name, distinguished by the namespace Wrox.GDIPlus6314.Ch12.DesignDropDownNumberTextBox for this example.

Here are the classes we need in our project:

  • The class representing the number textbox has identical code to the previous example, but this time we will name the class NumberTextBoxC to avoid confusion in the Toolbox.

  • Our custom property editor derived from UITypeEditor is similar to the NumberRangeEditor from the previous sample and will have the same name.

  • The NumberRange class used for data validation remains identical to the previous sample.

  • The NumberRangeDropDown is the drop-down control. This class is new and replaces the NumberRangeDialog class from the last sample.

  • The main form class we use to test the control, Form1, is identical to the last sample, except that it contains two NumberTextBoxC controls in place of the NumberTextBoxB ones.

Obviously, we won't bother discussing those classes that are unchanged in this section.

As usual we will create a new Windows Forms control project and make the usual name changes. This time we will name the solution and the DLL project DesignDropDownNumberTextBox.

The NumberRangeDropDown Control

Next, we are going to create the NumberRangeDropDown custom control. This will be the custom control in which we will edit the NumberRange structure. This custom control will be identical in nature to the composite custom control that we built in the last chapter. This will be a custom control that will have four constituent controls.

On the menu, select Project | Add User Control and set the name of the control to NumberRangeDropDown.cs. Add two Label controls, and two TextBox controls, and lay them out in a similar fashion to the following:

Click To expand

Now set the following properties for the TextBox controls:

Property | textBox1 | textBox2
Text |  |
(Name) | tbLow | tbHigh

Select the NumberRangeDropDown custom control by clicking on the design window somewhere outside any of the controls and set the following property for it:

Property | Value
BackColor | ControlLight

Now we need to edit the code using the code editor view.

Bring up the code editor for the NumberRangeDropDown custom control and add the following private member variables to the NumberRangeDropDown class:

private NumberRange originalNR;
private NumberRange newNR;
private bool canceling;

These variables will store respectively the old value of the NumberRange, the new value the user has typed in, and whether the user has exited the control in a manner that requires the new value not to be saved (in other words, they hit the Esc key).

Add the following public property to the NumberRangeDropDown class, which allows access to the new value of the NumberRange:

public NumberRange NewNumber Range
{
  get
  {
    return newNR;
  }
}

Modify the constructor of the NumberRangeDropDown class to take a NumberRange structure as an argument:

public NumberRangeDropDown (Number Range originalNR)
{
  this.originalNR = originalNR;

  // This call is required by the Windows.Forms Form Designer.
  InitializeComponent();
}

Double-click on the design window for the custom control (but not on one of the constituent controls) to add a Load event handler for the custom control. This event will need to initialize the two textboxes with the old NumberRange value. So, modify the Load event handler as follows:

private void NumberRangeDropDown_Load(object sender, System.EventArgs e)
{
   // Save a copy of NumberRange, so that if the user presses the
   // Esc key, we can revert to the previous values.
   newNR = originalNR;

   // Load up the TextBox controls.
   this.tbHigh.Text = originalNR.High.ToString();
   this.tbLow.Text = originalNR.Low.ToString();
}

Add a Validating event handler for the tbLow TextBox and ensure it is hooked up to the Validating event for tbLow:

private void tbLow_Validating(
  object sender, System.ComponentModel.CancelEventArgs e)
{
  try
  {
    Double.Parse(tbLowText);
  }
  catch (Format Exception)
  {
    MessageBox.Show("Invalid value", "Validation Error",
      MessageBoxButtons.OK, MessageBoxIcon.Error);
    e.Cancel = true;
    return;
  }
}

Do the same thing for tbHigh:

private void tbHigh_Validating(
  object sender, System.ComponentModel.CancelEventArgs e)
{
  try
  {
    Double.Parse(tbHigh.Text);
  }
  catch (FormatException)
  {
    MessageBox.Show("Invalid value", "Validation Error",
      MessageBoxButtons.OK, MessageBoxIcon.Error);
    e.Cancel = true;
    return;
  }
}

Now we need to consider what happens when the control closes.

Since this is a drop-down parameter editor, it will automatically close in three situations:

  • If the user presses the Enter key

  • If the user presses the Esc key

  • If the user clicks outside the custom control

The behavior that we want is that if the developer using NumberRangeDropDown to enter validation parameters presses the Enter key, or clicks outside the custom control window, the changed parameter values are committed. However, if the developer presses the Esc key, any changes to the parameter values should be discarded.

In order to implement this, we need to do the following:

  • Save the previous values in the Load event for the NumberRangeDropDown custom control.

  • Override ProcessDialogKeys() virtual method in the NumberRangeDropDown custom control. If this method gets an event for the Esc key, it needs to set a flag indicating that the user pressed the Esc key.

  • In the Leave event, we set the NumberRange property, unless the user pressed the Esc key.

Implement the ProcessDialogKey() method like this:

protected override bool ProcessDialogKey(Keys keyData)
{
  if (keyData == Keys.Escape)
  {
    originalNR = newNR;
    canceling = true;
  }
  return base.ProcessDialogKey(keyData);
}

Add an event handler for the Leave event, and modify it like this:

private void NumberRangeDropDown_Leave(
  object sender, System.EventArgs e)
{
  if (! canceling)
  {
    newNR.Low = Double.Parse(tbLow.Text);
    newNR.High = Double.Parse(tbHigh.Text);
  }
}

The NumberRangeDropDown control is now finished. All we have to do is add the code to tell Visual Studio .NET that this control is the editor for our NumberRange property.

The NumberRangeEditor

As before the NumberRangeEditor is a plain C# class derived from UITypeEditor. The code for it is fairly different from the previous version, so we will go through it in detail, but in the code samples, we will highlight only those lines of code that are different from the NumberRangeEditor version in the previous sample. We still need to override the same methods, EditValue() and Get Edit Style(), but our implementations are somewhat different. First, the required namespace references and class declaration:

using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace Wrox.GDIPlus6314.Ch12.DesignDropDownNumberTextBox
{
  /// <summary>
  /// Summary description for NumberRangeEditor.
  /// </summary>
  public class NumberRangeEditor : System.Drawing.Design.UITypeEditor
  {

Now let's examine the GetEditStyle() method:

public override
  UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
  if (context != null && context.Instance != null)
  {
    return UITypeEditorEditStyle.DropDown;
  }
  return base.GetEditStyle(context);
}

Here, we return a value to indicate that the control is a DropDown control.

Now for the EditValue() method:

public override object EditValue(
ITypeDescriptorContext context, IServiceProvider provider, object value)
{
  IWindowsFormsEditorService edSvc = null;

  if (context != null
    && context.Instance != null
    && provider != null)
  {
    edSvc = (IWindowsFormsEditorService)provider.GetService(
      typeof(IWindowsFormsEditorService));

    if (edSvc != null)
    {
      NumberTextBoxC originalControl = (NumberTextBoxC)context.Instance;
      NumberRangeDropDown editingDropDown =
        new NumberRangeDropDown
        (originalControl.NumberRange);
      edSvc.DropDownControl(editingDropDown);
      value = editingDropDown.NewNumberRange;
      return value;
    }
  }
  return value;
}

As you can see from this code, getting a reference to the instance of our NumberTextBoxC custom control is identical to the last version of NumberRangeEditor. However, instead of creating an instance of the dialog box, we create an instance to the drop-down custom control.

The EditValue() method of NumberTextBoxParametersEditor uses a different technique from the previous example to put up the custom property editor. In the previous version, the EditValue() method used the ShowDialog() method. This version uses the IWindowsFormsEditorService.DropDownControl() method:

edSvc.DropDownControl(editingDropDown);

Whereas in the last version, the dialog box was able to return a value, either DialogResult.OK or DialogResult.Cancel, in this version, we can't get a return value from the NumberRangeDropDown custom control. However, we have a public property in the NumberRangeDropDown custom control that contains the edited values:

value = editingDropDown.NewNumberRange;
return value;

I coded the NumberRangeDropDown custom control so that if the developer using the control presses the Esc key, the NumberRange property is not modified. Therefore, we can simply set the value object to the NewNumberRange property of the NumberRangeDropDown custom control. The behavior will be as expected.

We are done with our custom control. Compile it and add it to the Toolbox, remembering to remove previous versions of the control from the Toolbox window to avoid any conflict.

Creating a Test Application

Now build a test application, using the same test form as the previous example, except using NumberTextBoxC controls instead of NumberTextBoxB controls. Bring up the Properties window and press the down arrow that appears when you click on the NumberRange property. It should look like this:

Click To expand


Implementing a Custom Designer

Sometimes, editing properties is not the most convenient way to modify the configuration of a custom control. For example, if we were making an elaborate spreadsheet custom control, it certainly would be possible to enter the widths of the columns into the Properties window. However, this would be cumbersome at best and error-prone at worst. It would be far more convenient to allow the developer using the control to directly manipulate the column widths using the mouse. To get this functionality, we implement a custom designer-that is to say, a class that can give Visual Studio .NET the information it needs to directly draw your control in the Design window for you to edit it.

There is a range of functionality that we can implement using a custom designer. Some of the most important functionality is:

  • We can draw adornments on top of the control. The routine that draws adornments gets the chance to do its drawing after the control has drawn itself.

  • We can set the sizing and movement behavior of the control. We can choose whether the control can be sized by its top, left, bottom, or right sides. We can specify that the control may or may not be moved.

  • We can write code to set the default values for any property. As an example, we might use this if default values for one control depend on property values in another control.

  • We can write code to respond to mouse-dragging within the control. Our example of a spreadsheet control that allows the developer to directly manipulate column widths would be a good example of the use of this feature.

  • We can add items to the context menu that developers using the custom control see when they right-click on the control.

  • If our custom control is a composite control, allowing other controls to be dropped on it, we can manipulate and query the constituent controls.

We are not going to cover all aspects of custom designers, but we will cover enough so that you get an idea about their capabilities, and how to implement them. For the rest of this chapter, we are going to develop two examples that illustrate how to hook a control up to a custom designer. The first sample, SimpleBlankControl, is about as simple a control as you can get-all it does is paint its backdrop yellow. We will attach this to a designer that causes the Visual Studio .NET Design view to add a menu item to its context menu for the control allowing you to set the control's size to make it square. The second example is a bit more complex and features a control that can draw a vertical line. The corresponding designer allows you to move the line in the Visual Studio .NET Design view by dragging it.

The SimpleBlankControl

Our custom control, SimpleBlankControl, is very simple-all it does is draw itself yellow. The significance of this is, of course, not in the control itself but in the fact that you can edit it in the Visual Studio .NET Design view, for example changing its size by dragging with the mouse.

Building the Control

You shouldn't need any reminding by now how to create a Visual Studio .NET project containing the control. We need to create the project in the usual way and make sure the control is called SimpleBlankControl and derived from Control.

The only change you need to make to SimpleBlankControl is to use the Properties window to change its BackColor property to Yellow, and then modify the code for the class declaration by adding an attribute:

namespace Wrox.GDIPlus6314.Ch12.SimpleBlankControl
{
  /// <summary>
  /// Summary description for UserControl1.
  /// </summary>
  [Designer(typeof(SimpleBlankControlDesigner))]
  public class SimpleBlankControl : System.Windows.Forms.Control
  {

The DesignerAttribute indicates which class can be used as a custom designer. Here, it indicates a class that we are about to code up, SimpleBlankControlDesigner.

Building the Designer

The SimpleBlankControlDesigner class derives from the ControlDesigner class. This particular version simply adds a menu item to the context menu. Since the SimpleBlankControl class is derived from Control, Visual Studio .NET will already supply the ability to draw the control in the Design view and resize it.

Add a new class to the project using Project | Add Class…and add the following namespaces:

using System;
using System.Design;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;

We must also add a reference to the System.Design assembly to our project in order to access the ControlDesigner class from which our class is derived. The complete definition of the class looks like this:

public class SimpleBlankControlDesigner :
  System.Windows.Forms.Design.ControlDesigner
{
  public override DesignerVerbCollection Verbs
  {
    get
    {
      DesignerVerb[] verbs = new DesignerVerb[]
    {
      new DesignerVerb("&Make Square", new EventHandler(OnMake Square))};
      return new DesignerVerbCollection(verbs);
    }
  }

  private void OnMakeSquare(object sender, EventArgs e)
  {
    Size s = this.Control.Size;
    s.Width = Math.Max(s.Width, s.Height);
    s.Height = Math.Max(s.Width, s.Height);
    PropertyDescriptorCollection properties =
      TypeDescriptor.GetProperties(this.Control.GetType())
    PropertyDescriptor sizeProp = properties["Size"];
    sizeProp.SetValue(this.Control, s);
  }
}

As you can see, this class has one property and one method.

The Verbs property does the actual adding of the menu item, and is overridden from ControlDesigner. Visual Studio .NET knows about this method and uses it to find out about any extra items (verbs) that needs to be added to the context menu when the mouse is over the control in the design view. The property is of type DesignerVerbCollection-a class that represents a set of DesignerVerb instances. We won't worry too much about these new classes, other than to point out that a DesignerVerb object contains a string that indicates the text to be displayed in the context menu and an event handler that is invoked when the user clicks on that context menu item.

The OnMakeSquare() method is the event handler that we supply to the Make Square context menu item. Again, we won't go into the code in too much detail beyond saying that it identifies the larger dimension of the control (width or height) and sets both dimensions to this value, thus, making the shape of the control square. Note that if we want to change a property of our control, we should do it using a PropertyDescriptor. If we change the property directly, the Design window may not properly redraw itself.

Testing the Project

As usual, we compile the control and add it to the Toolbar. Create a test Windows Forms application and drag a SimpleBlankControl instance onto it. You will then find that if you right-click on the control in the Design view, the Make Square menu option will appear:

Click To expand

The SimpleLineControl

Our next version of a custom designer will be somewhat more complicated. First, we will make a custom control that draws a vertical line at the position of a property:

Click To expand

We will add a designer to this control, which will add an adornment to our control-a small rectangle. We will allow the developer using the control to click and drag on our adornment. As the developer drags on the adornment, we will change the value of the property that controls where the line is, so that at run-time, the line will get drawn at the position where the developer places it in the Design view.

When we look at this custom control in the designer, there is a small rectangle on the line. It looks like this in the designer:

Click To expand

We can move our mouse into that rectangle, and then drag. When dragging, the code changes the LinePosition property of the DragLine control, and then causes it to redraw itself.

Building the SimpleLineControl

As before, we create a new project with a custom control, this time named SimpleLineControl. We add the Designer attribute to the control definition:

namespace Wrox.GDIPlus6314.Ch12.SimpleLineControl
{
  [Designer(typeof(SimpleLineControlDesigner))]
  public class SimpleLineControl : System.Windows.Forms.Control
  {

We also change the BackColor property of the control to yellow and add the following field and property to indicate the position of the vertical line:

private int linePosition = 5;

[Category("Appearance")]
[Defaultvalue(5)]
public int LinePosition
{
  get
  {
    return linePosition;
  }
  set
  {
    linePosition = value;
  }
}

Then we add a Paint event handler to the form and modify the code as follows:

private void SimpleLineControl_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.DrawLine(Pens.Black, LinePosition, 0,
    LinePosition, ClientRectangle.Height);
}

The SimpleLineControlDesigner

In this custom designer, we will add code to draw the small rectangle adornment, and to respond when the user drags it. We create the code by adding a simple C# class to the project, called SimpleLineControlDesigner.cs. We then add the following namespace references:

using System;
using System.Design;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Windows.Forms;

Next, we define the class and add a couple of member fields and some constants:

namespace Wrox.GDIPlus6314.Ch12.SimpleLineControl
{
  public class SimpleLineControlDesigner :
    System.Windows.Forms.Design.ControlDesigner
  {
    private bool dragging = false;
    private int lastXPos = −1;
    bool mouseInAdornment = false;

    private const int WM_MOUSEMOVE      = 0×0200;
    private const int WM_LBUTTONDOWN    = 0×0201;
    private const int WM_LBUTTONUP      = 0×0202;
    private const int WM_LBUTTONDBLCLK  = 0x0203;
    private const int WM_RBUTTONDOWN    = 0×0204;
    private const int WM_RBUTTONUP      = 0×0205;
    private const int WM_RBUTTONDBLCLK  = 0×0206;

Of the fields, dragging indicates whether we are currently in a drag operation, lastXPos indicates the horizontal position of the line while being dragged, while mouseInAdornment indicates if the mouse is currently inside the adornment. All of these fields will get updated as the user moves the mouse, as we will see soon.

The constants that we define are the values of certain low-level Windows messages. Due to the way the control designer architecture has been designed, we won't be able to use the Windows Forms events that you are used to, to handle mouse movements and clicks in the Design view. Instead, we have to use a traditional-style message handler method, known as a windows procedure, or WndProc, which is what is happening behind the scenes in Windows Forms events anyway. C++ and ex-C++ developers will no doubt recognize the message names, but they may come as a surprise to VB and ex-VB developers. Don't worry too much. We will see how they are used soon. Basically, a message handling routine (equivalent to an event handler) is called, but instead of there being a different handler for each event there is one routine, and it is passed a number (known as a message) that indicates what has happened. Thus, for example, if the user has moved the mouse, it gets passed the number 0×0200 (=512 in decimal), this value having been traditionally given the name WM_MOUSEMOVE. The reason for not using .NET events here may be that it saves defining a large number of possible event handlers in the ControlDesigner class.

Next, we override the ControlDesigner.OnPaintAdornments() method, which Visual Studio .NET calls after painting the control to paint any adornments. We also have a helper method, calcAdornmentRect(), which figures out whereabouts to draw the rectangle. The rectangle should be drawn 4 pixels to the left of the line position and 5 pixels from the top of the control, and it should have size (8, 12):

private Rectangle calcAdornmentRect()
{
  int XPos = ((SimpleLineControl)(this.Control)).LinePosition;
  return new Rectangle(XPos - 4, 5, 8, 12);
}

protected override void OnPaintAdornments(PaintEventArgs pe)
{
  Graphics g = pe.Graphics;
  Rectangle r = calcAdornmentRect();
  g.FillRectangle(Brushes.White, r);
  g.DrawRectangle(Pens.Black, r);
}

Note that we obtain the actual SimpleLineControl being painted as the ControlDesigner.Control property.

We also override the ControlDesigner.OnSetCursor() method that is responsible for changing the mouse cursor. The semantics that we must implement are a bit strange. If we have changed the mouse cursor, this method needs not to call up to the base class. If we haven't changed the mouse cursor, then we must call up to the base class. As we will see when we examine the WndProc() method, we always set the cursor ourselves if the either the mouse is over the adornment rectangle, or the developer is dragging the line. Hence, our OnSetCursor() override looks like this:

protected override void OnSetCursor()
{
  if (mouseInAdornment == false | | ! dragging)
    base.OnSetCursor();
}

Now comes the final method in our class-and this one's a biggy. It is overridden from ControlDesigner and it is that Windows procedure, which is called in lieu of mouse events. Its parameter is a Message struct, whose Msg property gives the message value that indicates why the method has been called. Like traditional Windows procedures, the structure of this method is basically a big switch statement depending on the message passed to it:

protected override void WndProc(ref Message m)
{
  Point p;
  Rectangle r;
  SimpleLineControl dl;

  switch (m.Msg)
  {
    case WM_LBUTTONDOWN:
      r = calcAdornmentRect();
      p = new Point(m.LParam.ToInt32());
      if (r.Contains(p))
      {
        dragging = true;
        this.Control.Parent.Cursor = Cursors.Arrow;
      }
      else
      {
        base.WndProc(ref m);
      }
      break;

    case WM_MOUSEMOVE:
      p = new point(m.LParam.ToInt32());
      dl = (SimpleLineControl)(this.Control);
      if (dragging)
      {
        int XPos = p.X;
        XPos = Math.Max(5, XPos);
        XPos = Math.Min(dl.ClientRectangle.Width - 5, XPos);
        if (lastXPos != XPos)
        {
          PropertyDescriptorCollection properties =
            TypeDescriptor.GetProperties(this.Control.GetType());
          PropertyDescriptor sizeProp = properties["LinePosition"];
          sizeProp.SetValue(this.Control, XPos);
          lastXPos = XPos;
          dl.Invalidate();
          dl.Update();
        }
        this.Control.Parent.Cursor = Cursors.Arrow;
        return;
      }
      else
      {
        r = calcAdornmentRect();
        p = new Point(m.LParam.ToInt32());
        if (r.Contains(p))
        {
          mouseInAdornment = true;
          this.Control.Parent.Cursor = Cursors.Arrow;
        }
        else
        {
          mouseInAdornment = false;
        }
        base.WndProc(ref m);
      }
      break;

    case WM_LBUTTONUP:
      if (dragging)
      {
        dl = (SimpleLineControl)(this.Control);
        p = new Point(m.LParam.ToInt32());
        int XPos = p.X;
        XPos = Math.Max(5, XPos);
        XPos = Math.Min(dl.ClientRectangle.Width - 5, XPos);
        PropertyDescriptorCollection properties =
          TypeDescriptor.GetProperties(this.Control.GetType());
        PropertyDescriptor sizeProp = properties["LinePosition"];
        sizeProp.SetValue(this, Control, XPos);
        lastXPos = −1;
        dl.Invalidate();
        dl.Update();
        dragging = false;
      }
      base.WndProc(ref m);
      break;

    default:
      base.WndProc(ref m);
      break;
  }
}

The above code basically checks what message has been passed to it, and then does the following:

  • If it has been called because the user has clicked the left mouse button down, then we check if the mouse is in the adornment rectangle. If so, we record that a dragging operation has just started, otherwise, we pass control to the base class.

  • If the user has moved the mouse, then we check whether we are in the middle of a dragging operation. If so, we need to redraw the control with the vertical line and adornment in the new dragged position. Otherwise, we don't need to do anything besides make a note of whether the mouse is in the adornment rectangle.

  • If this method has been called because the left mouse button has been released, then we need to check if we were in a dragging operation. If so, then we need to record that the drag operation has finished, and update the LinePosition property of the control we are editing so that it contains the new location of the vertical line.

The WndProc() method needs to set a state variable (the dragging variable). The mouse-down event sets it, and the mouse-move and mouse-up events use the state of the dragging variable. This is a common method for programming dragging operations at the lowest level.

When the method changes the value of LinePosition, it calls the Invalidate() and the Update() methods on the custom control, which causes the control to re-draw itself:

dl.Invalidate();
dl.Update();

Note that any message that our WndProc() override doesn't process needs to be passed to the base class.

This is, of course, a very artificial example. A real example, such as a spreadsheet control that lets the user directly manipulate column widths and move columns around would be hundreds of lines of code, and the code would obscure the relevant points being made here. There is nothing fundamentally different between what we would do in an elaborate control, and what we have done here.

That completes the sample. To build the example, you will need to add a reference to the System.Design.dll assembly. As before, to try it out, you will need to create a test Windows Forms application and place a SimpleLineControl on the form.


Summary

In this chapter, we saw how we could enhance the design-time experience of developers using custom controls that we design. We saw that we could significantly change the way the developer using our control interacts with it in Visual Studio .NET.

A very important and relatively simple method of enhancing design-time support is to create a TypeConverter for our custom control. A TypeConverter provides three fundamental areas of functionality:

  • It converts to and from strings, so that a data structure such as a structure or class can be displayed as a string in the Properties window. If feasible, the developer can also set the values for a structure or class by entering a string.

  • It provides information on sub-properties, so that the developer can expand the property in the Properties window, and edit the sub-properties.

  • It provides information to Visual Studio .NET, so that the code generator can properly generate the code to persist the values in the properties of the control.

We also covered creation of a custom user interface for editing properties. There are two styles. With the first style, the developer using the control edits the properties in a modal dialog box. With the second style, the developer using the control edits the properties in another custom control that appears in the drop-down position below the property being edited.

For very complex custom controls, or those that we want to provide more direct manipulation of the custom control, we can implement a custom designer. With a custom designer, we can allow the developer using the control to directly manipulate it using the mouse. We also might provide more information about the control in the control, so that the developer doesn't need to look at the Properties window to get a quick overview of all of the controls on a window.

With previous custom control technologies, much of the design-time functionality was embedded in the design tool. With Visual Studio .NET and the .NET Framework, all design-time functionality is encapsulated by the custom control itself. This gives us unprecedented power to develop much more usable custom controls.


Chapter 13: Scrolling

Overview

Sometimes when building a custom control, we wish to create a virtual space that is larger than the physical space, and allow the user to scroll horizontally and vertically within the virtual space. The visible space might occupy an entire window, or it may occupy just a portion of a window. In either case, we can consider the visible space as a viewport into the larger virtual space. As an obvious example of this, when you are looking at your code in the code editor in Visual Studio .NET, the virtual space contains the entire code listing for the source code file you are examining. It is very rare that your code is small enough to fit in the code window, so the window has scrollbars to allow you to scroll through the code.

There is considerable in-built support for scrolling in the Windows Forms classes, but there are also situations in which you may prefer to handle scrolling yourself. We will take a look at what Windows Forms can do for you, and then examine some specific situations in which you might want to handle scrolling from your code instead of relying on the .NET Framework class library code.

We might expect this topic to be discussed earlier in this book, when we were focusing more on GDI+. In previous implementations of graphical programming toolkits, this would be the case. However, when using Windows Forms, by far the easiest and best way to put together a scrolling region is to implement it as a custom control. It is possible to build a scrolling region without first building a custom control, but it is much more cumbersome. For this reason, we delayed delving into this topic until we had covered building custom controls in the last two chapters.

If you are not comfortable with the process of building custom controls, please read Chapters 12 before continuing with this chapter. This chapter builds on the information presented in those chapters.


Building a Scrolling Sample

We are going to start off by building an extremely simple example that illustrates building a control capable of scrolling, using entirely the in-built support in the Windows Forms framework classes-it will simply be a form having a control that displays an image.

For this sample, create a Windows Forms application and call it ScrollingPicture. We won't even need to derive our own custom control-instead we will make use of the Panel class. So, drag a Panel control from the Toolbox onto the form. Then set the background image to some image file on your computer. Any file will do as long as it's a reasonably large image-big enough that it won't fit completely into the Panel on your form. For our example, we are using a JPG file of the side of a car.

At this point, running the sample should show something like this:

Click To expand

Everything displays fine, but the trouble is that, since the image is too big to fit in the panel, we have no way of seeing anything other than the top left corner of it. Obviously, we need scrollbars. To do this, we need to set two properties on the panel:

  • AutoScroll

  • AutoScrollMinSize

AutoScroll is a Boolean property that indicates whether the control should be prepared in principle to display scrollbars. So, locate this property in the Properties window and set it to True for the Panel control.

AutoScrollMinSize is of type Size. It stores the size of the virtual space. In other words, the total size you would ideally have liked the control to have been if your screen was big enough. Clearly there is no point in displaying scrollbars unless this size is bigger than the actual size of the control, and that is exactly what happens. The control will only display a vertical scrollbar if AutoScrollMinSize.Height is greater than its Size.Height property, and similarly for the horizontal scrollbar. AutoScrollMinSize is also used by the control when calculating how much to allow the user to scroll and how large to make the scrollbars.

You will find AutoScrollMinSize in the Properties window too, and you can set it there, but we won't for this example. Instead of putting a hard-coded value in for it, we will have the control calculate it at run-time from the size of the image it is displaying. So, make the following change to the constructor of the Form1 class:

public Form1()
{
  //
  // Required for Windows Form Designer support
  InitializeComponent();

  this.panel1.AutoScrollMinSize = this.panel1.BackgroundImage.Size;
}

Now run the application again and this will be the result:

Click To expand

You will find that you can scroll around to view the entire image.

Setting the Scrollbar Position

The code above allows the user to scroll, but there are times when we might, for some reason, want to set whereabouts the scrollbars are programmatically. For example, if you are writing a word processor, and you want to allow the user to search for some text, then presumably the program will respond to the search by moving the scrollbars so that the text in question is in the viewport. We will quickly develop the ScrollingPicture sample to illustrate how to do this. Instead of having the scrollbars set to show the top-left of the picture at startup, we will have the application start up showing the central part of the picture.

The position of the scrollbars can be set (or of course retrieved) using the AutoScrollPosition property. This property returns the coordinates of the top-left point of the viewport, relative to the top-left corner of the virtual space. For the screenshot above, this will be (0, 0).

It is not possible to set the scrollbar position until the form actually loads, so we must add an event handler for the Load event. Here is what it looks like:

private void Form1_Load(object sender, System.EventArgs e)
{
  int midX = this.panel1.AutoScrollMinSize.Width/2;
  int midY = this.panel1.AutoScrollMinSize.Height/2;
  int halfSizeX = this.panel1.Size.Width/2;
  int halfSizeY = this.panel1.Size.Height/2;
  int startPosX = midX-halfSizeX;
  if (startPosX < 0) startPosX = 0;
  int startPosY = midY-halfSizeY;
  if (startPosY < 0) startPosY = 0;

  this.panel1.AutoScrollPosition = new Point(startPosX, startPosY);
}

The bulk of this code is devoted to working out what the scroll position should be to show the center of the picture. The formula we use is to take the width of the virtual space minus the width of the control, divided by 2, for the horizontal scrollbar; and similarly for the vertical one. However, if this formula gives a value less than zero we set the position to zero (a value less than zero indicates that the control size is greater than the size of the virtual space so no scrollbar will be displayed anyway).

After adding this code, the sample looks like this when it starts:

Click To expand

Drawing in a Viewport

The ScrollingPicture sample was particularly simple because the Panel class handled all the drawing itself. However, if we are doing the drawing in our code, we will need to take account of the position of the scrollbars in our drawing code. For example, suppose we need to draw something (such as a string or a rectangle, or even use Graphics.DrawImage() to draw an image) at the point (100, 100) in the virtual space. If the scrollbars are set so the viewport shows the top-left corner of the virtual space, then it is fine to draw the item at point (100, 100). Suppose though, the user has scrolled 50 pixels down and 30 pixels across, so that the AutoScrollPosition property of the control is (30, 50). Then, the point (100, 100) in the virtual space corresponds to the point (70, 50) in the actual viewport, so the item actually needs to be drawn at location (70, 50). Although this sounds complicated, as we will see soon, this can be handled very simply with the Graphics.TranslateTransform() method.

In this section, we will develop another short example, called ScrollingText. This sample is similar to the previous one, except that it displays the string Hello, World, in four locations, which means we will be handling the drawing code ourselves. This is what the sample looks like:

Click To expand

This sample will also be different from the previous one to the extent that the control will handle its own scrollability rather than this being set by the containing form.

To create the project create a new Windows Control Library project called ScrollingText. Modify the code so that the control class is derived from Panel:

public class ScrollingText : System.Windows.Forms.Panel

Next, rename the file to ScrollingText.cs and set the (Name) property to ScrollingText.

Using the Properties window, set the BackColor property of the control to System.Window, the AutoScroll property to True, and the AutoScrollMinSize property to (600,400).

Now add a member field that will store the font for the text:

Font textFont = new Font ("Times New Roman", 24);

and add the following event handler for the Paint event:

private void ScrollingText_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.TranslateTransform(
    this.AutoScrollPosition.X, this.AutoScrollPosition.Y);
  g.DrawString("Hello, World" textFont, Brushes.Black, 40, 40);
  g.DrawString("Hello, World" textFont, Brushes.Red, 40, 240);
  g.DrawString("Hello, World" textFont, Brushes.Blue, 350, 40);
  g.DrawString("Hello, World" textFont, Brushes.Green, 350, 240);
}

The key line of code here is:

g.TranslateTransform(
  this.AutoScrollPosition.X, this.AutoScrollPosition.Y);

This shifts the origin, as far as drawing operations are concerned, by the same amount as the control is currently scrolled by. This means that the text will appear in the correct location whatever the position of the scrollbars.

To test the control, you simply need to create a Windows Form application and add the control to the form.


Scrolling Techniques

The examples we have seen so far give a flavor of how to have scrolling managed entirely by the Windows Forms classes supplied by Microsoft. You have also seen how simple it is to implement scrolling by simply setting a couple of properties on the control. So, you may be wondering why you would ever not want to code up scrolling in that way. Actually there are several reasons:

  • Not all the controls in the System.Windows.Forms namespace support scrolling. In order to support scrolling, the control must be derived directly or indirectly from the class System.Windows.Forms.ScrollableControl. This is the case for many classes, including TextBox, ListBox, and the Form class itself, but there are exceptions. Most importantly, the class Control does not support scrolling. The Panel class does, which is why in the above example, we derived our custom control from Panel rather than Control. Of course, if you wish, there is nothing to stop you deriving a custom control directly from ScrollableControl.

  • You may want to implement smooth scrolling, which is when you click to move the scrollbar by a large amount in one go, instead of the viewport jumping instantaneously to the new position, as happens with the scrolling supplied by the Microsoft Windows Forms classes. It smoothly scrolls there over an interval of perhaps half a second, giving your application a much more polished and professional appearance.

  • If you rely on a control deriving from ScrollableControl, and rely on its in-built scrolling capabilities, then you don't get any low-level control over the scrolling or over any actions that your control takes in response when the user scrolls. If you implement scrolling yourself, then you can access, for example, the events that occur when the user scrolls in order to provide your own handlers.

Another issue is the size of the virtual space. Some drawing techniques involve having an in-memory drawing surface that represents the entire virtual space and transferring the appropriate portion of that image to the screen as required, while other techniques involve simply drawing the contents of the viewport to the screen. Consider the first example that we developed in this chapter-the scrollable image example. Even though we were only viewing a portion of the image at one time, the entire image clearly had to remain in memory as the background image associated with the panel control. On the other hand, in the second example in which we were drawing text in the Paint event, there was no such area of memory. The only record of what had been drawn was the screen itself. If the virtual space is limited in size, as it was in both of our examples, then there is no problem with holding a representation of the entire virtual space in memory. However, for many applications, such as spreadsheets or word processors, the size of virtual space is potentially unlimited. In this case, it is not feasible to use a drawing technique in which the virtual space is permanently represented in memory.

If you do not want to use the ScollableControl scrolling support, then your usual technique will involve adding horizontal and virtual scrollbars to your control from the Toolbox-these are represented by the HScrollBar and VScrollBar classes. Then, you can add handlers to the various scrolling events raised by these classes in order to implement scrolling.

For the rest of this chapter, we will discuss a few of the techniques you might use to implement scrolling, and we will present examples to demonstrate a few of them.

Which Approach do I Use?

The approach that we take to building a scrolling control depends on the capabilities that we want. The questions to be answered regarding our custom control are:

  • Do we need to compose this custom control of other Windows Forms controls, such as buttons, textboxes, and so on?

  • Do we need to draw using GDI+ into the custom control?

  • Do we want to create a smooth scrolling virtual space? If we make an application that scrolls smoothly, it will have the appearance of ‘sliding' rather than jumping. This can give our application a polished appearance.

  • Is the virtual space limited in size, or is it possibly very large, such as we might use for a mapping custom control. In a mapping custom control, we may be able to pan over a very large area, and it may not be practical to create an image that contains the entire area over which we might pan.

How we code up scrolling for a given control depends to a large extent on the answers to these questions:

  • If we don't need to draw into our custom control using GDI+, we are composing this custom control of other controls, and the virtual space is limited in size, we should build our custom control by deriving from the UserControl class-which does derive from ScrollableControl. If we add child controls to our custom control, these are called constituent controls.

  • If we need to draw into our custom control using GDI+, and we are not composing this custom control with other controls, and the virtual space is limited in size, and we don't want to implement smooth scrolling, then we don't even need to build a custom control. We can create an instance of a PictureBox control, place it in a Panel control, draw into an image, and set the BackgroundImage property of our custom control to our built image. We will illustrate this technique soon. Alternatively, we can, as in our earlier samples, draw directly into a Panel or some similar control.

  • If we need to both draw into our virtual space and add constituent controls to it, and it is limited in size, we derive from the UserControl class, build an image, draw into the image, and set the BackgroundImage property of our custom control to our built image. We won't be illustrating this technique in this chapter but we will remark on it.

  • If we want to implement smooth scrolling, we derive from the UserControl class and add a horizontal and vertical scrollbar to our custom control. We build an image, draw into the image, and manage the drawing of the image in our custom control. This technique might be used in a multi-column list custom control. When the user scrolls up or down, instead of just having the list ‘jump' to the new location, we can incrementally scroll it by a few pixels at a time, and do the incremental scrolling quickly, so that it has the appearance of ‘sliding' to the new position. This provides the user with an additional visual clue about the navigation action just taken. We will illustrate this technique later in the chapter.


Placing a Non-Scrollable Control in a Panel

The next example shows another way to create a scrolling image. This example is quite simple, and most readers will probably know how to implement this by now, but for the sake of completeness, we will include it. It is very similar in concept to the previous examples, and will look identical to the ScrollText sample-it will display Hello, World in several locations in the virtual space, and allow the user to scroll between them. Instead of drawing to a scrollable control though, we will draw to a control that does not support scrolling-in this case PictureBox-and embed this control in a control that does-for this example, we will use Panel. We will arrange that, when the form is loaded, it draws the text into a bitmap, then loads this bitmap into the picture box. Doing things this way can improve performance when scrolling since it means that when the user scrolls, there is no need to redraw the new part of the viewport that appears-it has already been drawn and the image simply needs to be transferred from the bitmap in memory. This is known as a BitBlt operation, which we will explore in the next chapter, but bear in mind that it does store the image for the entire virtual space, and so, should only be used if the virtual space is limited in size.

Create a new Windows Application to the solution and name it PictureBoxScrollText. In a similar fashion to the previous test application, drag a Panel control onto the form, and we will set the same properties for it.

Now drop a PictureBox control into the Panel control (in the actual Panel, not in the main form) and set its Dock property to Fill so that it occupies the entire area of the Panel.

Use the Properties window to set the BackColor of the PictureBox to SystemWindow, and also set its Size to (600,400). This means that the PictureBox is too big to fit into the Panel so the Panel will need to have scrolling enabled. Again using the Properties window, set the AutoScroll property of the Panel to True and AutoScrollMinSize to (600,400).

Now we can modify the code for the Form1 class. First, add the following field:

public class Form1 : System.Windows.Forms.Form
{
  Font textFont = new Font("Times New Roman", 24);

Add a Load event handler for the Form and modify it like this:

private void Form1_Load(object sender, System.EventArgs e)
{
  Bitmap b = new Bitmap(600, 600);
  Graphics g = Graphics.FromImage(b);
  g.Fi1lRectangle(
    Brushes.White, new Rectangle(0, 0, b.Width, b.Height));
  g.DrawString("Hello, World", textFont, Brushes.Black, 40, 40);
  g.DrawString("Hello, World", textFont, Brushes.Red, 40, 240);
  g.DrawString("Hello, World", textFont, Brushes.Blue, 350, 40);
  g.DrawString("Hello, World", textFont, Brushes.Green, 350, 240);
  pictureBox1.BackgroundImage = b;
  pictureBox1.Size = b.Size;
}

This drawing code is very similar to that for the earlier ScrollText example, except that we no longer have to set the TranslateTransform() method for the Graphics object.

Compile and run the application, and the user can pan over the image that we drew. If the state of the application changes, we can draw again into the same image, or create a new image, draw into it, and set the new image on the PictureBox control.


Smooth Scrolling

Creating a smooth scrolling virtual space can give our application a very polished appearance. In addition to better aesthetics, smooth scrolling adds to the functionality of an application in that it gives the user one more visual clue about the navigation action just taken. They press the down arrow key, the content of the custom control slides up, and this subtly confirms that they pressed the key that they wanted to press. If the display simply jumped to a new position, the visual clue only says that we have moved-other than the new position of the scrollbar, there is no clue to confirm that the movement was up as opposed to down.

In this section, we will present an example that illustrates smooth scrolling. Our technique to implement smooth scrolling consists of creating an image that contains the graphical contents of our custom control and sliding the image appropriately behind a viewport.

The approach that we will take is to create a custom control that derives from UserControl. We will add a horizontal and vertical scrollbar as constituent controls. We will write a Paint event handler that draws the image as appropriate in the custom control. In addition, the Paint event handler draws the background color into any areas of the custom control not covered by the image.

When sliding the image, we need to draw the image numerous times at appropriate positions and times. We don't want to implement this using a process of invalidation and waiting for the Paint event to come through for each drawing of the image. Rather, we want to have a loop that does the drawing, and then uses the Sleep() method of the Thread class for each iteration of the loop. Once we involve the event queue, as we would if we were to invalidate and wait for the Paint event, the system performance is indeterminate. There is no guarantee that the Paint event will come through on schedule. This could cause the slide to appear less smooth.

The SmoothScrollableControl Sample

The sample looks very similar to previous samples. It displays the text, Hello, World, in several locations in the viewport:

Click To expand

To code the sample, create a new Windows Control Library and call it SmoothScrollableControl. Rename UserControl1.cs to SmoothScrollableControl.cs and, similarly, rename the class.

Note that because we are implementing the scrolling ourselves, there is no need to derive from UserControl. The application will work the same way whether we derive from UserControl, Panel, or Control. However, leaving it as UserControl gives us a slightly more user-friendly interface in the Design view in Visual Studio .NET. Hence, we will keep our control as derived from UserControl.

Now use the Toolbox to add an HScrollBar and a VScrollBar to our control, and set the Dock property to Bottom and Right respectively. We should also use the Properties window to rename these scrollbars to hScrollBar and vScrollBar respectively.

Next, we need to make some changes to the code for the control.

Firstly, add a System.Threading namespace reference. We will need the System.Threading.Thread class to control the timing for the smooth scrolling:

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using System.Threading;

Now add the following fields to the class:

public class SmoothScrollableControl : System.Windows.Forms.UserControl
{
  private Image scrollingImage;
  private Point originOfImage = new Point(0, 0);

scrollingImage will contain the bitmap for the virtual space, while originOfImage stores the coordinates of the viewport.

We will add a public property to allow client code to access the scrolling image:

public Image Scrolling Image
{
  get
  {
    return scrollingImage;
  }
  set
  {
    scrollingImage = value;
    adjustScrollBars();
  }
}

and another property that gives us the area of the control that is actually available for drawing our image in-that is, the client area minus the area taken up by the scrollbars:

public Rectangle ScrollingImageRectangle
{
  get
  {
    return new Rectangle(0, 0, this.vScrollBar.Left,
      this.hScrollBar.Top);
  }
}

We will also modify the constructor:

public SmoothScrollableControl()
{
  // This call is required by the Windows.Forms Form Designer.
  InitializeComponent();

  this.SetStyle(ControlStyles.Opaque, true);
}

Setting the style to ControlStyles.Opaque makes drawing more efficient and prevents flickering by preventing the control from drawing a background color-there is no need to do this since our bitmap and the scrollbars will, between them, entirely cover the viewport.

Next, we will look at the process of painting. First of all, here is a helper method that works out the portion of the client area corresponding to the viewport (as we have just seen, that is the client area excluding the scrollbars), and draws the appropriate portion of the bitmap to just this area:

private void drawImage(Graphics g, int x, int y)
{
  g.SetClip(ScrollingImageRectangle);

  Rectangle drawRect =
    new Rectangle(new Point (x, y), ScrollingImageRectangle.Size);
  g.DrawImage(this.scrollingImage,
    ScrollingImageRectangle, drawRect, GraphicsUnit.Pixel);
}

Now let's examine the Paint event handler. Note that this handler is not called when the user scrolls. As we will soon see, the event handlers for scrolling events call the drawImage() helper method directly to update the display. This event handler is called whenever there is some other reason why the control needs to be repainted (for example, it has just been restored from a minimized state). It deals with copying the relevant portion of the bitmap to the screen, but also whites out any other areas to the right of and below the image that might need repainting:

private void SmoothScrolling_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  Brush bgb = new SolidBrush(this.BackColor);

  if (this.scrollingImage != null)
  {
    drawImage(g, this.originOfImage.X, this.originOfImage.Y);

    g.ResetClip();

    Rectangle r;

    // Paint area to right of image and left of the vertical scrollbar.
    r = new Rectangle(scrollingImage.Width, 0,
      this.vScrollBar.Left - scrollingImage.Width,
        this.ScrollingImageRectangle.Height);
    if (! r.IsEmpty)
      g.FillRectangle(bgb, r);

    // Paint area below image and above horizontal scrollbar.
    r = new Rectangle (0, scrollingImage.Height,
      this.ScrollingImageRectangle.Width,
        this.hScrollBar.Top - scrollingImage.Height);
    if (! r.IsEmpty)
      g.FillRectangle(bgb, r);

    // Paint the small rectangle at the bottom right of the control.
    r = new Rectangle(this.hScrollBar.Right, this.vScrollBar.Bottom,
     this.ClientRectangle.Width - this.hScrollBar.Right,
     this.ClientRectangle.Height - this.vScrollBar.Bottom);
    g.FillRectangle(bgb, r);
  }
  else
  {
    g.FillRectangle(bgb, this.ClientRectangle);
  }
}

Lets look at the code that handles the scrollbars themselves. First up is a helper method that calculates the correct lengths for the bars, and sets various properties on them that describe how far they should be allowed to scroll:

private void adjustScrollBars()
{
  if (scrollingImage != null)
  {
    hScrollBar.Minimum = 0;
    hScrollBar.Maximum = scrollingImage.Width;
    hScrollBar.LargeChange = ScrollingImageRectangle.Width;
    hScrollBar.SmallChange = ScrollingImageRectangle.Width / 10;
    hScrollBar.Value = originOfImage.X;
    vScrollBar.Minimum = 0;
    vScrollBar.Maximum = scrollingImage.Height;
    vScrollBar.LargeChange = ScrollingImageRectangle.Height;
    vScrollBar.SmallChange = ScrollingImageRectangle.Height / 10;
    vScrollBar.Value = originOfImage.Y;
  }
}

We will look at the scrolling event handlers soon. The handlers both call another helper method, slide(), which handles the process of sliding the scrollbars from a given old position to a given new position (the positions being stored as the coordinate of the top-left corner of the viewport, relative to the top-left corner of the virtual space). The slide() method is the one where the smooth scrolling action actually takes place. It enters a loop. At each iteration of the loop, we redraw the viewport in an incrementally scrolled position, then use the Thread.Sleep() method to wait for 10ms (0.01 of a second) before moving to the next iteration:

private void slide (Point oldP, Point newP)
{
  Graphics g = this.GreateGraphics();

  if (oldP.X != newP.X)
  {
    // Slide horizontally,
    bool goingUp = newP.X - oldP.X > 0;
    int delta = (newP.X - oldP.X) / 12;
    if (goingUp && delta < 1)
      delta = 1;
    if (!goingUp && delta > −1)
      delta = −1;
    int i = oldP.X;
    while (true)
    {
      if (i == newP.X)
        break;
      i += delta;
      if (goingUp && i > newP.X)
      {
        i = newP.X;
        continue;
      }
      if (!goingUp && i < newP.X)
      {
        i = newP.X;
        continue;
      }
      drawImage(g, i, oldP.Y);
      if (i != newP.X)
        Thread.Sleep(10);
    }
  }
  if (oldP.Y != newP.Y)
  {
    // Slide horizontally.
    bool goingUp = newP.Y − oldP.Y > 0;
    int delta = (newP.Y − oldP.Y) / 12;
    if (goingUp && delta < 1)
      delta = 1;
    if (!goingUp && delta > −1)
      delta = −1;
    int i = oldP.Y;
    while (true)
    {
      if (i == newP.Y)
        break;
      i += delta;
      if (goingUp && i > newP.Y)
      {
        i = newP.Y;
        continue;
      }
      if (!goingUp && i < newP.Y)
      {
        i = newP.Y;
        continue;
      }
      drawImage(g, oldP.X, i);
      if (i != newP.Y)
        Thread.Sleep(10);
    }
  }
}

Now we move on to the event handlers for the scrolling events. To add the vertical scrolling event handler, click on the vertical scrollbar in the design window in Visual Studio .NET. Then, use the Properties window to locate the Scroll event and double-click on it to add an event handler. The Scroll event is raised whenever the user clicks on the scrollbars to scroll. Its handler is passed a ScrollEventArgs argument. ScrollEventArgs supplies a property, Type, which indicates the nature of the scroll event. ScrollEventType.SmallIncrement and ScrollEventType.SmallDecrement indicate that the user clicked on one of the small arrows at the ends of scrollbars to perform an incremental scroll. The LargeIncrement and LargeDecrement values indicate that the user clicked in the light area in the scrollbar to move the scrollbar by a large amount, or actually clicked and dragged the scrollbar itself.

You should add the following code for the Scroll event handler for the vertical scrollbar:

private void vScrollBar_Scroll(
  object sender, System.Windows.Forms.ScrollEventArgs e)
{
  switch (e.Type)
  {
    case ScrollEventType.SmallIncrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.Large Increment:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.SmallDecrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.LargeDecrement:
      Point oldOrigin = originOfImage;
      originOfImage = new Point(originOfImage.X, e.NewValue);
      slide(oldOrigin, originOfImage);
      break;
    default:
      originOfImage = new Point(originOfImage.X, e.NewValue);
      this.Invalidate();
      break;
  }
}

Similarly, add this code for the horizontal scrollbar:

private void hScrollBar_Scroll(
  object sender, System.Windows.Forms.ScrollEventArgs e)
{
  switch (e.Type)
  {
    case ScrollEventType.SmallIncrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.LargeIncrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.SmallDecrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.LargeDecrement:
      Point oldOrigin = originOfImage;
      originOfImage = new Point(e.NewValue, originOfImage.Y);
      slide(oldOrigin, originOf Image) ;
      break;
    default:
      originOfImage = new Point(e.NewValue, originOfImage.Y);
      this.Invalidate();
      break;
  }
}

We have one final method to add, which is an event handler for the Resize event on the SmoothScrollableControl control. The reason for adding this handler is that, clearly, if the control gets resized, then we will need to recalculate the scrollbar positions and possibly redraw the viewport:

private void SmoothScrollableControl_Resize(
  object sender, System.EventArgs e)
{
  if (scrollingImage != null)
  {
    int x = Math.Min(originOfImage.X,
      scrollingImage.Width - ScrollingImageRectangle.Width);
    x = Math.Max(x, 0);
    int y = Math.Min(originOfImage.Y,
      scrollingImage.Height - ScrollingImageRectangle.Height);
    y = Math.Max(y, 0);
    originofImage = new Point(x, y);
  }
  adjustScrollBars();
  this.Invalidate();
}

You can now build the control and add it to the Toolbox.

Testing the Control

Add a new Windows Application called TestSmoothScrollableControl to the project and make it the startup project. Add a reference to the custom control in the test application and place a SmoothScrollableControl on the form's surface.

The SmoothScrollableControl class itself implements all the functionality required to display a virtual space in a viewport with smooth scrolling. Beyond that though, it is a general-purpose class. It doesn't specify the image to go in the virtual space. It is up to the containing form to do that. So, we need to add a Load event to the form and modify the code as follows:

private void Form1_Load(object sender, System.EventArgs e)
{
  Bitmap b = new Bitmap(600, 600);
  Graphics g = Graphics.FromImage(b);
  g.FillRectangle(Brushes.White,
    new Rectangle(0, 0, b.Width, b.Height));
  g.DrawString("Hello, World",
    new Font("Times New Roman", 24), Brushes.Black, 40, 40);
  g.DrawString("Hello, World",
    new Font("Times New Roman", 24), Brushes. Red, 350, 40);
  g.DrawString("Hello, World",
    new Font("Times New Roman", 24), Brushes.Blue, 40, 240);
  g.DrawString("Hello, World",
    new Font("Times New Roman", 24), Brushes.Green, 350, 240);
  g.DrawString("Hello, World",
    new Font("Times New Roman", 24), Brushes.Aquamarine, 40, 440);
  g.DrawString ("Hello, World",
    new Font("Times New Roman", 24), Brushes.Magenta, 350, 440);
  smoothScrollableControll.ScrollingImage = b;
}

Now we can compile and run the application. Then, click on the scrollbars to see the smooth scrolling. Pressing the cursor left/right/up/down keys on the keyboard also causes smooth scrolling to take place.


Summary

In this chapter, we saw several methods for creating a scrolling virtual space. In almost all of these cases, it is far easier to create a scrolling virtual space if we use custom controls to implement it.

We saw how to build virtual spaces with the following characteristics:

  • The virtual space can contain a number of constituent controls. We implement this by creating a custom control and deriving from the UserControl class.

  • If we want also to draw graphics into a virtual space that contains constituent controls, we can draw into an image, and set the BackgroundImage property for our custom control.

  • If we only have graphics, and the size of our virtual space is limited, we don't even need to make a custom control. We can draw into an image, set the BackgroundImage property for a PictureBox control, put the PictureBox control into a Panel control, and set some properties on the Panel control.

  • If we want to create a smooth scrolling virtual space, we derive from the UserControl class, and handle the scrollbar events ourselves. We presented such an implementation in this chapter.


Chapter 14: Mouse Events and Cursors

Overview

Many custom controls allow the user to interact with custom controls using the mouse. Programming to enable mouse interaction is easy, but there are a few details to pay attention to.

Most programmers reading this book are familiar with mouse events, and probably have written event handlers for them in one or another graphical user interface programming library. Therefore, we won't spend much time on the simplest of mouse event handling, but will instead concentrate on the more advanced issues, primarily those that pertain to building custom controls. For the most part, handling mouse events in Windows Forms is very similar in nature to handling mouse events in MFC and other toolkits. There are not a lot of surprises, but there are a few.

In this chapter, we will cover the following areas:

  • Mouse events- We will briefly recap the mouse events available in .NET.

  • Mouse cursors- One of the key design principles of graphical user interfaces is that we always want to give the user as much visual feedback as possible. Mouse cursors are a great way to provide visual clues for them. There are a variety of stock mouse cursors that we can use to help the user not to become confused. In addition, we can create custom mouse cursors to fit the style and use of our custom control. We will discover how to do this in this chapter.

  • Hit testing- The activity of determining the location of the mouse cursor over our custom control. When the user is moving the mouse over our custom control (without the mouse button pressed), we can alter the appearance of our custom control, or we can change the mouse cursor to give them a clue that they can take some action at certain locations in our custom control. When the user presses or releases the mouse button, we use hit testing to determine what action we should take.

  • Mouse event routing- When no mouse button is pressed, mouse events are sent to the window that the mouse is over. When a mouse button is held down then the mouse, by default, is said to be captured, and all events will go to the window in which the mouse button was originally pressed. There are rules about which window they are sent to, particularly when the mouse is captured. We can alter this behavior to a certain extent. We will examine these rules, and how to alter the behavior. In addition, there are some bugs that we should avoid, particularly when the mouse is captured.

  • Drawing while dragging- Sometimes, when performing a drag-and-drop operation, we will want to show the users what they are dragging. We may want to use an alpha technique, so that what we are dragging is semi-transparent. We will be going through the details of this later.

  • Drag-scrolling- Sometimes, we want to implement scrolling behavior when the user selects something in the control, and drags the mouse outside of the control. This is the behavior that we see in a word processor when we select text, and drag the mouse below the window in which we are editing text. The text in the window starts scrolling as it continues selection. We can also see this behavior in web browsers, image editors, and many other types of applications. We will refer to this action in this book as drag-scrolling (which is not a standard Windows term). There are some details to which we need to pay attention when implementing this functionality, and we will present an example that illustrates this.

Note that in this chapter, we will routinely refer to the mouse button or the left mouse button. In both cases this means the mouse button that is used for click events. With the default Windows settings, this will be the left mouse button though, obviously, if you have set your computer up for a left-handed person, this will be the right button. We will not consider context menu events or the context mouse button (normally the right button) at all in this chapter.


Mouse Events

There are seven mouse events. These are events defined in the Control class. Most Windows Forms classes, including the Form class derive from the Control class. These mouse events are:

Event | Description
MouseDown | Raised when the cursor is over a control and the user presses a mouse button.
MouseEnter | Raised when the cursor enters a control while no mouse button is pressed.
MouseHover | Raised when the mouse cursor hovers over a control without moving.
MouseLeave | Raised when the mouse cursor leaves a control.
MouseMove | Raised when the mouse cursor moves over a control.
MouseUp | Raised when the mouse button is released. By default, this event always goes to the control in which the mouse button was pressed.
MouseWheel | Raised when the user moves the mouse wheel while the control has the focus.

If the user moves the mouse cursor into a control and takes some action, the normal sequence of events, depending on exactly what the user does, normally runs something like MouseEnter, MouseMove, MouseHover/MouseDown/MouseWheel, MouseUp, MouseLeave.

In addition, it's worth being aware of the following events:

Event | Meaning
Click | User has clicked the mouse button
DoubleClick | User has double-clicked the mouse button

These higher-level events each encapsulate several lower-level events. For example, the Click event is raised when the user has depressed and then released the left mouse button. These events can be useful for simple work. However, if you want your application to behave in a sophisticated manner with regard to mouse events, you will probably want to use the lower-level events such as MouseDown in order to gain a finer degree of control. Accordingly we will be focusing on the lower-level events in this chapter.


Mouse Cursors

Every mouse cursor has what is called the hot spot. This is the exact point in the cursor bitmap that corresponds to the mouse's actual location-that is, the location that will be passed to event handlers as the mouse location. For the standard arrow cursor, the hot spot is the tip of the arrow. The hot spot in an I-Beam cursor is a point on the I-Beam, just above the bottom end:

Click To expand

It is important to be aware that the hot spot is not necessarily at the top left of the cursor, as it could theoretically be any point within the area of the cursor, and will be in different positions for different cursors. As shown above, the standard arrow cursor gives a very clear visual clue to the user about where the hotspot is, but that isn't the case for the I-Beam.

The Cursor class encapsulates mouse cursor functionality. We can create a cursor from a file or a Stream. In addition, there are a number of standard cursors that we can get from static properties of the Cursors class. If we set the cursor to Cursors.Default, the cursor is the default cursor, which is usually the arrow cursor.

Standard Mouse Cursors

There are a large number of standard mouse cursors available. You can obtain any of these via static properties of the Cursors class. For example, to set the cursor of the current control (which we will assume is accessed via the this reference) to a hand, you can use:

this.Cursor = Cursors.Hand;

while to set it to a cross, you would code:

this.Cursor = Cursors.Cross;

The Cursors properties that return different cursors are:

AppStarting | Arrow | Cross | Hand | Help | HSplit
IBeam | No | NoMove2D | NoMove Horiz | NoMoveVert | PanEast
PanNE | PanNorth | PanNW | PanSE | PanSouth | PanSW
PanW | SizeAll | SizeNESW | SizeNS | SizeNWSE | SizeWE
UpArrow | VSPlit | WaitCursor |  |  |

In addition, a property called Cursors.Default returns the default cursor.

The CursorsExample

The following example demonstrates the use of standard cursors. It is a simple Windows application that displays four rectangles. Each rectangle is associated with a different cursor, and as the mouse is moved over each rectangle the cursor changes accordingly. If we click the mouse, the wait cursor is displayed for one second and processing of mouse events (other than that the mouse cursor still moves around the screen as the user moves the mouse) is suspended for that time. The screenshots below shows the application in action:

Click To expand

Create a new Windows Application called CusorsExample. Add a MouseMove event handler to the form, and modify it like this:

private void Form1_MouseMove (object sender, System.EventArgs e)
{
   Point p = new Point(e.X, e.Y);
   Rectangle r;

   if (new Rectangle(0, 0, 100, 100).Contains(p))
     this.Cursor = Cursors.Cross;
   else if (new Rectangle(100, 0, 100, 100).Contains(p))
     this.Cursor = Cursors.Hand;
   else if (new Rectangle(0, 100, 100, 100).Contains(p))
     this.Cursor = Cursors.VSplit;
   else if (new Rectangle(100, 100, 100, 100).Contains(p))
     this.Cursor = Cursors.UpArrow;
   else
     this.Cursor = Cursors.Default;
}

Note that because the MouseMove event is raised so often, it is very important to do as little processing as possible in it. In the above case our handler looks a bit dubious at first sight because we are continually setting the mouse cursor-in fact, this is not a problem since all we are doing is setting one reference.

Now add a Paint event handler and modify it like this:

private void Form1_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.FillRectangle(Brushes.Blue, 0, 0, 100, 100);
  g.FillRectangle(Brushes.Red, 100, 0, 100, 100);
  g.FillRectangle(Brushes.Yellow, 0, 100, 100, 100);
  g.FillRectangle(Brushes.Green, 100, 100, 100, 100);
}

Run the application and as we move the mouse over the window, the mouse cursor changes. If we move the mouse over the area to the right of the colored squares, the mouse cursor changes to the default cursor. If we move the mouse over the window borders, the cursor changes to the appropriate resize cursor.

Sometimes, we may want to change the cursor to the WaitCursor for the entire application for a certain amount of time, perhaps when doing an extensive amount of processing. To do this, we can set the static Cursor.Current property to Cursors.WaitCursor. Setting Cursor.Current is different from setting the Cursor property of the current control for two reasons:

  • This assignment in Cursor.Current is effective only until the current event handler returns, or until our code calls the Application.DoEvents() method.

  • Assigning Cursor.Current to any value other than the default cursor, Cursor.Default, prevents processing of mouse events while the temporary cursor is in place.

This behavior is what we want-we can set the cursor to the Cursors.WaitCursor, and then forget about it. After the processing is done, we return from our event handler, and the cursor reverts to the correct cursor. To see this functionality, add the following two lines to the Click event handler for the form:

private void Form1_Click(object sender, System.EventArgs e)
{
  Cursor.Current = Cursors.WaitCursor;
  System.Threading.Thread.Sleep(1000);
}

This changes the cursor to the WaitCursor for one second.

Custom Mouse Cursors

We can create our own custom cursor. The easiest way to do this is to start with an existing cursor. You can get an existing cursor by searching for files with a .cur extension in the Microsoft Visual Studio .NET directory on the drive where your Visual Studio .NET IDE is installed.

Alternatively, if you have downloaded the examples for this book from the Wrox web site, you can take a look at the cursor named MyCursor.cur, which looks like this:

Click To expand

We can define certain pixels to be transparent. With the above cursor, all pixels other than the black and white pixels are transparent. The hot spot is the point in the middle. When viewed in the image editor in Visual Studio .NET, the transparent pixels are blue-green. We can modify the hot spot using the Set Hot Spot Tool, which is a button on the toolbar that is visible when we are in the image editor:

Click To expand

Place the cursor in the directory where your application will execute (in other words ...\CursorExample\bin\Debug) and modify the example as follows:

private void Form1_MouseMove(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  Point p = new Point(e.X, e.Y);
  Rectangle r;
if (new Rectangle(0, 0, 100, 100).Contains(p))
  this.Cursor = new Cursor("MyCursor.cur");
else if (new Rectangle(100, 0, 100, 100).Contains(p))
  this.Cursor = Cursors.Hand;
   .         .         .
   .         .         .
   .         .         .

Run the example and move the mouse over the upper-left rectangle to see the custom cursor.


Hit Testing and Drawing During Mouse Events

Hit testing is the process where we determine if a particular point is in a given rectangle, region, or path. We saw an example of this in CursorsExample, where we used the Rectangle.Contains() method:

if (new Rectangle(0, 0, 100, 100).Contains(p))
  this.Cursor = Cursors.Cross;

When we are hit testing in regions or paths as opposed to rectangles, we use the Region.IsVisible() or GraphicsPath.IsVisible() methods, which respectively test whether a point is located inside the given region, or graphics path.

With today's fast computers, there is a rule of thumb that we should not prematurely optimize our code. A lot of unnecessary ugliness has been introduced into otherwise clean code in the name of optimization. This tendency is understandable given that many of us came from slow computing environments in the past where, if we didn't proactively optimize, the application would be unusable. Now, primarily we strive to create the cleanest and most maintainable code possible. We do any necessary optimization after we have implemented the main functionality.

However, I make an exception when writing graphical code, where I attempt to draw exactly what is necessary, and no more. I have two main reasons for this philosophy.

First, once we involve the graphics subsystem, performance is not deterministic. An operation that runs quickly on one computer with a sophisticated graphics card runs slowly on another. The memory available can be important here too. This is compounded by object-oriented techniques where we build abstractions on abstractions, and the farther away we get from the lowest-level abstractions, the more we forget the actual processing cost.

The second reason is that when we involve the graphics subsystem, behavior is not deterministic. Where one graphics card may not mind at all if we fill a particular rectangle many times, another may cause flickering.

Therefore, I write graphics code with a bit of optimization. Primarily, this involves keeping state and only drawing if the state changes.

The following example demonstrates hit testing on the three primary data types that describe areas. In addition, it demonstrates keeping state and drawing only if the state changes.

The HitTestExample

The hit testing example consists of a form in which three shapes are drawn. These shapes change color as the mouse moves:

Click To expand

Whenever the mouse enters any shape, it gets colored in-pink for the double rectangle shape, blue for the trapezoid, and green for the lower rectangle. As the mouse moves out of each shape, it gets restored to white.

Create a new Windows Application and call it HitTestExample. Modify the using statements section to look like the following:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

Add the following private member variables to our Form1 class:

private GraphicsPath path1;
private GraphicsPath path2;
private Region reg;
private Rectangle rect;
private bool inPath = false;
private bool inRegion = false;
private bool inRect = false;

path1 represents the trapezoid and path2 the double rectangle, rect will store the coordinates of the lower rectangle.reg will represent the double rectangle. We use a Region object just to illustrate use of the Region class, although we will have to construct it using the GraphicsPath path2. The bool variables simply indicate whether the mouse was in each of the shapes the last time the MouseMove event was raised.

Modify the constructor to initialize our member variables:

public Form1()
{
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent();

  path1 = new GraphicsPath();
  path1.AddLine(10, 10, 50, 10);
  path1.AddLine(50, 10, 50, 40);
  path1.CloseFigure();
  path1.AddLine(50, 40, 10, 50);

  path2 = new GraphicsPath();
  path2.AddLine(70, 10, 130, 10);
  path2.AddLine(130, 10, 130, 40);
  path2.AddLine(130, 40, 160, 40);
  path2.AddLine(160, 40, 160, 60);
  path2.AddLine(160, 60, 110, 60);
  path2.AddLine(110, 60, 110, 40);
  path2.AddLine(110, 40, 70, 40);
  path2.CloseFigure();
  reg = new Region(path2);

  rect = new Rectangle(10, 70, 80, 40);
}

The above code simply creates the shapes. As we just mentioned, we can only construct a region from a path, which is why we create and initialize the path2 variable, then construct the region variable from it.

Next, add a Paint event handler, which will draw the shapes in white, like this:

private void Form1_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e. Graphics;
  g.FillRectangle(Brushes.White, Client Rect angle);
  g.DrawPath(Pens.Black, path1);
  g.DrawPath(Pens.Black, path2);
  g.DrawRectangle(Pens.Black, rect);
}

Finally, add a MouseMove event handler as follows. This is the handler where all the work is done:

private void Form1_MouseMove(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  Graphics g = this.CreateGraphics();

  try
  {
    if (inPath)
    {
      if (! path1.IsVisible(e.X, e.Y))
      {
        g.FillPath(Brushes.White, path1);
        g.DrawPath(Pens.Black, path1);
        inPath = false;
        return;
      }
    }
    else if (inRegion)
    {
      if (! reg.IsVisible(e.X, e.Y))
      {
        g.FillRegion(Brushes.White, reg);
        g.DrawPath(Pens.Black, path2);
        inRegion = false;
        return;
      }
    }
    else if (inRect)
    {
      if (! rect.Contains (e.X, e.Y))
      {
        g.FillRectangle(Brushes.White, rect);
        g.DrawRectangle(Pens.Black, rect);
        inRect = false;
        return;
      }
    }
    if (! inPath)
    {
      if (path1.IsVisible(e.X, e.Y))
      {
        g.FillPath(Brushes.LightBlue, path1);
        g.DrawPath(Pens.Black, path1);
        inPath = true;
        return;
      }
    }
    if (! inRegion)
    {
      if (reg.IsVisible(e.X, e.Y))
      {
        g.FillRegion(Brushes.Pink, reg);
        g.DrawPath(Pens.Black, path2);
        inRegion = true;
        return;
      }
    }
    if (! inRect)
    {
      if (rect.Contains(e.X, e.Y))
      {
        g.FillRectangle(Brushes.LightGreen, rect);
        g.DrawRectangle(Pens.Black, rect);
        inRect = true;
        return;
      }
    }
 }
 finally
 {
  g.Dispose();
 }
}

The MouseMove event handler looks complex because it draws only what is necessary-it doesn't redraw with every MouseMove event. It only draws if the state changes. Let's look at it in more detail.

Firstly note that all the code is contained in a try…finally block. This is to ensure that the Graphics object is disposed of correctly, as there are multiple return statements in the code. The presence of the finally block will ensure that whenever the computer hits a return statement in the try block, processing will transfer to the finally block before the handler exits.

We start off in the routine by examining the inPath field that tells us if the mouse was previously inside the trapezoid shape-in which case, that shape will be currently colored. If this is the case, then we check whether the new mouse position is outside the trapezoid. If it is, then we redraw the trapezoid white. Since the mouse can't jump between shapes, in this case it must be outside the borders of the other shapes, so we know there is nothing that needs to be done with the other shapes and we can return:

if (! inPath)
{
  if (path1.IsVisible(e.X, e.Y))
  {
    g.FillPath(Brushes.LightBlue, path1);
    g.DrawPath(Pens.Black, path1);
    inPath = true;
    return;
  }
}

The rest of the code simply looks at all the remaining possibilities for where the mouse can be. Notice how we have carefully optimized this code because it is in the MouseMove handler-we return as soon as we possibly can. Checking if a point is in a region is a mathematically complex operation so we don't want to do that any more times than necessary.

In addition, this example demonstrates the technique of creation and destruction of a Graphics object during the processing of the MouseMove event. This is in contrast to a Paint event handler, where a Graphics object is passed as part of the PaintEventArgs argument. Remember, if we create it, we must dispose of it at the end of the method. Notice that we do our drawing in the event handler itself rather than calling Invalidate(). Drawing during the mouse event can be a little different from drawing during a Paint event in this regard. For the most part, when drawing during a mouse event, for performance reasons, we might not want to go through the Invalidate-Paint cycle. Primarily, we want to draw directly to the drawing surface, in the mouse event handler itself. Although, having said that, any substantial painting should certainly go into the Invalidate-Paint cycle or you risk losing responsiveness. In the end, there are no hard rules, and you should be aware of and assess the suitability of both possible techniques when coding up each application.

Build and run the application. As we move the mouse over the rectangle, region, and path, it is filled with a color.


Mouse Event Routing

When no mouse button is down, mouse events are sent to the window that the mouse is over. When a mouse button is pressed, until it is released, the mouse is ‘captured'. This means that all subsequent MouseMove events, until the mouse is released, are sent to the window in which the mouse button went down, even if the mouse cursor moves outside of the window. If the mouse moves outside of the window, then the location of the mouse move events will be outside of the window, but they will still go to the window in which the button went down. In addition, the MouseUp event will go to the same window, which is very important. Often, a custom control will note that the mouse went down in the control. The custom control expects that it will receive the MouseUp event so that it can change state, noting that the mouse button is no longer down. However, if the MouseUp event went to a different window, then the custom control would be left thinking that the mouse button was still down. We can occasionally see this bug in commercial applications, and we don't want to make the same mistake in ours.

There is an exception to this rule. If we were implementing a drag-and-drop operation (but not using the built-in drag-and-drop functionality in Windows Forms) that could drop outside of the window in which the mouse button went down, after the MouseDown event we would set the Capture property of the Control class to false. This will change the behavior so that the MouseMove events and the MouseUp events will go to whatever window the mouse is over.

To demonstrate the issues involved with capturing the mouse, and how to solve those issues, we will create a small application that emulates the appearance and behavior of a push button.

First, we need to discuss the logic for creating a button:

  • If the mouse button goes down outside of the button rectangle, we ignore the event.

  • If the mouse button goes down inside the button, we set a Boolean variable named mouseDownInButton to true. In addition, we draw the button with a ‘down' appearance, and set another Boolean variable, buttonDown to true.

  • If the mouse moves outside of the button while the mouse is still down, we set buttonDown to false, and draw the button with an ‘up' appearance.

  • If the mouse moves back inside of the button while the mouse is still down, we set buttonDown to true, and draw the button with a ‘down' appearance.

  • If the user releases the mouse outside of the button, we take no action apart from setting mouseDownInButton to false.

  • If the user releases the mouse inside of the button, we generate an event. We set mouseDownInButton to false. We draw the button with an ‘up' appearance.

The above logic points out the problem if our control didn't receive the MouseUp event. In this case, the mouseDownInButton Boolean will not be set to false, and the control will incorrectly think that it still needs to process the MouseMove events.

The RoutingMouseEvents Sample

This sample is aimed at illustrating mouse routing. It displays a form on which two rectangles have been drawn, as in the screenshot below:

Click To expand

The top rectangle is our simulated button-it is designed to act like a button, which, when clicked, displays a message box saying that it has been clicked. However, instead of using a real button control, we simulate it by drawing the rectangle and handling the required mouse events from the main form class.

If we press the left mouse button over our pseudo-button, then we see it's sunken appearance:

Click To expand

Perhaps not so realistic, but it is enough for our purposes! The dialog box doesn't get displayed until the left button is released, completing the ‘click'.

The lower rectangle is a pretend battery. It pops up a dialog saying Your battery is fully charged, when the mouse moves over it. The reason that rectangle is there is because it will allow us to demonstrate a subtle problem with mouse capturing in which there are some situations where our form can lose the mouse capture. Before we discuss that problem (and present the solution), however, we will go over the code for the application.

Create a new Windows Application, call it RoutingMouseEvents, and modify the Form1.cs module, as per the following instructions.

First, we need a reference to the GraphicsOutlineModel namespace since we will be using our GraphicsOM library discussed in Chapter 10, with the documentation available as part of the code download from the Wrox.com website.

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using Wrox.GraphicsOutlineModel;

Recall that the GraphicsOM class contains routines for drawing 3D rectangles, which we will make use of when drawing our pseudo-button. You will also need to add a reference to the GraphicsOutlineModel assembly in the project.

Next, we need to add some fields to the Form1 class:

public class Form1 : System.Windows.Forms.Form
{
  /// <summary>
  /// Required designer variable.
  /// </summary>
  private Rectangle buttonRect;
  private bool mouseDownInButton;
  private bool buttonDown;

The purposes of these fields should be self-explanatory. They store the coordinates of the rectangle where the pseudo-button will be placed, as well as whether the mouse left button has been depressed in the pseudo-button, and whether the pseudo-button is down.

Next, we initialize the rectangle that defines our pseudo-button:

public Form1()
{
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent();
  buttonRect = new Rectangle(10, 10, 100, 40);
}

Now we add a routine that will draw the pseudo-button. Notice how in the following code we check for a null Graphics reference and if so, instantiate a temporary Graphics object. The pseudo-button is actually drawn by drawing a couple rectangles inside each other using the GraphicsOM class to create the raised or sunken effect depending on the up parameter passed to this method:

private void drawButton(Rectangle rect, Graphics g, bool up)
{
  bool disposeGraphics = false;
  if (g == null)
  {
    g = this.CreateGraphics();
    disposeGraphics = true;
  }
  GraphicsOM g2 = new GraphicsOM(g);
  g2.DrawRectangle(Pens.Black, rect);
  Rectangle innerRect = new Rectangle(rect.Location, rect.Size)
  innerRect.Inflate(new Size(−1, −1));
  if (up)
    g2.Draw3DRectangle(innerRect, ThreeDStyle.Raised, 2);
  else
    g2.FillRectangle(Brushes.LightGray, innerRect);
  if (disposeGraphics)
    g.Dispose();
}

Next, it is time to add event handlers. First, a handler for the Paint event, which you should wire up to the event in the normal way. This handler simply draws the pseudo-button and the battery rectangle:

private void Form1_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  drawButton(buttonRect, g, true);
  // Draw the rectangle that simulates
  // "Your battery is fully charged."
  g.FillRectangle(Brushes.Red, new Rectangle(10, 150, 100, 40));
}

The MouseDown event handler simply checks whether the mouse button has been depressed in the pseudo-button and, if so, redraws the button in a pressed state and updates the mouseDownInButton and buttonDown fields accordingly:

private void Form1_MouseDown(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  if (e.Button == MouseButtons.Left && buttonRect.Contains(e.X, e.Y))
  {
    mouseDownInButton = true;
    buttonDown = true;
    drawButton(buttonRect, null, false);
  }
}

Similarly the MouseUp handler checks if the mouse button had previously been depressed in the pseudo-button, in which case we know a mouse click is being completed, so we need to display the dialog box and update the appearance of our ‘button':

private void Form1_MouseUp(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
 if (mouseDownInButton && e.Button == MouseButtons.Left
 {
   drawButton(buttonRect, null, true);
   mouseDownInButton = false;
   if (buttonRect.Contains(e.X, e.Y))
     MessageBox.Show("Button was pressed");
 }
}

The MouseMove event handler is a bit more complex:

private void Form1_MouseMove(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  if (mouseDownInButton )
  {
    if (buttonRect.Contains(e.X, e.Y))
    {
      if (! buttonDown)
      {
        drawButton(buttonRect, null, false);
        buttonDown = true;
      }
    }
    else
    {
      if (buttonDown)
      {
        drawButton(buttonRect, null, true);
        buttonDown = false;
      }
    }
   }
   if (new Rectangle(10, 150, 100, 40).Contains(new Point(e.X, e.Y)))
     MessageBox.Show("Your battery is fully charged.");
}

In this handler, we first check whether we are in the middle of a mouse-click-in other words, the mouse button had been depressed while in our ‘button' on the form. If so, then we need to redraw the button correctly, according to whether the user has moved the mouse outside its area. Finally, we check if we are in the battery area and if so display the appropriate message box.

Handling the CaptureChanged Event

The application we have just created works great almost all of the time. We can press the button with the left mouse button, drag the mouse outside of the window, and release the button outside of the window. Since the mouse was captured, the MouseUp event went to our Form1_MouseUp event handler, which changed state appropriately by setting the mouseDownInButton variable to false.

However, if another window is put up, perhaps not even by our application, the MouseUp event will go to that window, even though the mouse is captured. If we move the mouse over the red rectangle in our form, the example puts up a modal dialog box, simulating what would happen if such a dialog box were put up by another application. To see an example of this bug, press the left mouse button in our pseudo-button, and without releasing the mouse button, drag the mouse over the red rectangle. A message box is put up. Dismiss the message box (using the Enter key, say), and drag the mouse back over our pseudo-button. Since the MouseUp event went to the message box, our pseudo button still thinks that it has not received a MouseUp event, and behaves as though the mouse were down.

Another problem can be caused by the user doing something, albeit something quite unusual. Press the left mouse button in our pseudo-button. Now, press and release the right mouse button, while continuing to hold down the left mouse button. Next, drag the mouse cursor outside of the window, and release the left mouse button. If we now drag the mouse back over our pseudo button, we can see that the pseudo button thinks that the left mouse button is still down, even though it is not.

What happened is this. Pressing and releasing the right mouse button caused the mouse to be no longer captured. When we released the left mouse button while outside of the window, the MouseUp event went to whatever window the mouse was over, and our pseudo-button didn't receive it.

There is a solution, which is to watch for the WM_CAPTURECHANGED event-a Win32 message. Windows raises this message whenever the capture state of the mouse changes, no matter how it changes. However, we have to dip down into the Win32 API to watch for this event since Windows Forms don't define a corresponding .NET event.

We need to make the following additions to the code for the MouseRoutingEvent form. First, add a delegate definition inside the Form1 class. This will define the event handler for a when a WM_CAPTURECHANGED message occurs.

public delegate void CaptureChanged();

Now we define an inner class, again inside the Form1 class, to handle the low-level Windows procedure that will respond to the WM_CAPTURECHANGED message:

class CaptureChangedWindow : NativeWindow
{
  public CaptureChanged OnCaptureChanged;

  protected override void WndProc(ref Message m)
  {
    if (m.Msg == 533)     // WM_CAPTURECHANGED
      OnCaptureChanged();
    base.WndProc(ref m);
  }
}

Inside the Windows API, windows are represented by numbers known as Window Handles. Normally this is all dealt with internally by the Form class. However, if you want to work at a low level, Microsoft makes available the System.Windows.Forms.NativeWindow class. This has none of the extensive windowing features of the Form class, but it does wrap a native Windows handle and provides a virtual method, WndProc(), that represents the Windows procedure called when messages are received. Hence, we define our CaptureChangedWindow class to derive from NativeWindow, and override NativeWindow.WndProc() to provide a handler for the WM_CAPTURECHANGED message (which has value 533). Our override of this method simply invokes the CaptureChanged delegate.

Next, we declare a member field to hold a CaptureChangedWindow instance:

public class Form1 : System.Windows.Forms.Form
{
  public delegate void CaptureChanged();
  CaptureChangedWindow ccw;

Now we define a method in Form1 that will be invoked when the CaptureChanged delegate is invoked. This is what we do when the form detects a capture change; set mouseDownInButton to false to indicate we are no longer in a potential mouse-click event:

private void CaptureChangedEventHandler()
{
  mouseDownInButton = false;
}

Finally, we need to modify the Form1 constructor to create and initialize the nested class:

public Form1()
{
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent();
  buttonRect = new Rectangle(10, 10, 100, 40);

  ccw = new CaptureChangedWindow();
  ccw.AssignHandle(Handle);
  ccw.OnCaptureChanged +=
    new CaptureChanged(CaptureChangedEventHandler);

}

The line:

ccw.AssignHandle(Handle);

initializes ccw with the same window handle that our Form1 instance contains, which means that messages sent to Form1 that would normally cause events to be raised will additionally be passed to ccw for processing in our WndProc() override.

That's all we need to do. Now our application behaves correctly, regardless of whether we press and release the right mouse button while the left mouse button is held down, or whether we pass the mouse cursor over the rectangle that simulates a modal dialog opening while the mouse is captured.

One point that is worth noting is that if we press and release the right mouse button while the left mouse button is held down, the pseudo-button will remain pressed no matter where we move the mouse then, even if we release both mouse buttons. Although this behavior is unusual, the circumstances that cause it are exceptional. It is peculiar to Windows Forms controls and not exhibited by pre-.NET code. It appears to be linked to the architecture of Windows Forms event handling and there is no easy way to avoid it.

We are now going to leave mouse events behind for a while and take a look at GDI. The reason is that our next example, which will demonstrate dragging some text around the screen with the mouse, will need to use a couple of functions from the old GDI API. So, before we can introduce that example, we will need to take a quick look at what GDI is and, in particular, at the BitBlt() function.


GDI and BitBlt

Before the next sample we are going to need to introduce the BitBlt() method, which comes from the old GDI API. GDI was the forerunner of GDI+. It serves the same purpose, allowing you to draw graphics and manipulate bitmaps, but it works at a lower-level, and is not .NET-aware. It consists of a large number of C-style functions, and if you want to call them from C# or Visual Basic .NET code, you will need to use the DllImport attribute and the PInvoke mechanism. It is not the purpose of this book to explain GDI, but in this section, we will just give you enough information that you can use the BitBlt() and a few other simple GDI functions, since we will be using them in the next examples.

BitBlt is often pronounced BitBlit and stands for Bitmap Block Transfer, which is the name for the way that many images are copied to the screen. A bitmap, or a rectangular area within a bitmap is copied across from one drawing surface to another using support from the graphics card hardware, which means the operation occurs very fast. A BitBlt is what actually happens internally in the Graphics.DrawImage() method. A drawing surface can exist in the computer's memory, or on the screen, or can represent a printer, and BitBlt operations can be used equally well to transfer data between any of these types of surface. Clearly, the process of drawing to the screen normally means performing a BitBlt from the computer's memory to the screen memory.

Each drawing surface is represented in Windows by a data structure known as a device context. The device context contains all the information needed to tell Windows how to draw to that surface, as well as a reference to where the memory containing the data for all the pixels in that surface is. The Graphics object, which you have been using routinely, is actually a .NET wrapper class around a device context.

Why Use GDI?

The main reason for using GDI in some situations is that there are some things you can do with GDI that you can't do with GDI+. Here we will examine the main issues:

  • Working at a low level

  • Performance

  • Different Raster Operations

Working at a Low Level

Firstly, GDI+ is a very high-level class library. This makes it very quick for you to write basic drawing code, but it has the disadvantage that sometimes, if you want to do something unusual, you might find that there is simply no way to perform the task in GDI+. A case in point, and the real reason why we will be using GDI in part for the next examples, is that the GDI+ classes do not allow you to copy data from the screen to an image contained in memory. There is no fundamental reason why you shouldn't be able to do that-it's just the GDI+ classes have been written in a way that doesn't support BitBltting from the screen. If you use GDI though, you can do so. When you call the BitBlt() function, you are required to separately supply the device contexts for the source and destination surfaces. By contrast, although Graphics.DrawImage() does a BitBlt() internally, the destination device context is defined as being the one represented by the Graphics object you are using. The source device context will be one that is internally created in GDI+ specifically to represent the image that you are copying, and will therefore necessarily be a memory device context. This extra flexibility can sometimes give GDI the edge and make available new techniques to us.

Performance

You may also find that in a few cases, using GDI might give you a performance edge because working at a lower level gives you more control over what the computer is doing. Graphics.DrawImage() provides another example of this. The internal implementation of this method is undocumented, but there is at least a possibility that each call causes a new device context to be temporarily created. Using GDI, if you are repeatedly drawing from the same bitmap, you would be able to cache the device context.

On the other hand, you should take care with using GDI for performance. Unless there is a clear optimization that you can perform using GDI, then you may find GDI+ actually performs better. That's because, as a newer API, it is likely to be better supported by Microsoft in the future, and indeed, Microsoft is already claiming that GDI+ can offer better performance than GDI. Also, GDI+ has some newer features not available in GDI (because when GDI was written there was no possibility of any hardware supporting those features)-an example of this is gradient brushes defined by paths. In general, our advice is to stick to GDI+ unless in a particular situation there is a clear reason for dropping to GDI.

Different Raster Operations

Up to now, we have viewed BitBltting as a simple case of copying rectangular areas. In fact, we can do quite a bit more with the BitBlt() method than just copying pixels from one location to another. In addition, it is possible to set each destination pixel to some value based on combining the source pixel and the original color in the destination pixel, which can give rise to a variety of interesting effects. WinGdi.h defines the following constants that define these effects:

Operation | Hex Code | Description
SRCCOPY | 0x00CC0020 | dest = source
SRCPAINT | 0x00EE0086 | dest = source OR dest
SRCAND | 0x008800C6 | dest = source AND dest
SRCINVERT | 0x00660046 | dest = source XOR dest
SRCERASE | 0x00440328 | dest = source AND (NOT dest)
NOTSRCCOPY | 0x00330008 | dest = (NOT source)
NOTSRCERASE | 0x001100A6 | dest =(NOT source) AND (NOT dest)
MERGECOPY | 0x00C000CA | dest = (source AND pattern)
MERGEPAINT | 0x00BB0226 | dest = (NOT source) OR dest
PATCOPY | 0x00F00021 | dest = pattern
PATPAINT | 0x00FB0A09 | dest = DPSnoo
PATINVERT | 0x005A0049 | dest = pattern XOR dest
DSTINVERT | 0x00550009 | dest = (NOT dest)
BLACKNESS | 0x00000042 | dest = BLACK
WHITENESS | 0x00FF0062 | dest = WHITE

In the above table, the AND, OR, and NOT operations are bit-wise operations performed on the RGB values that describe the color at each pixel.

However, in this book, we will only need to use the SRCCOPY operation. As mentioned earlier, our main reason for dropping to GDI will be so that we can transfer data from the screen to memory.

If you are using GDI+ and DrawImage(), then these raster operation flags are not available-you can only copy pixels.

Using BitBit()

The technique to use BitBlt() involves the following:

  • Declare a DllImport attribute to specify the DLL location that contains the implementation of the BitBlt() method.

  • Declare the external BitBlt() method. This consists of the signature of the method, including its return value, and the types of all of its arguments.

  • Before we use the BitBlt() method, we get a handle to the device context (Hdc) for our drawing surface. To get the handle, we call the GetHdc() method of the Graphics object for our drawing surface.

  • We then can call the BitBlt() method, passing the Hdc as an argument.

  • After we are done with the Hdc, we need to remember to release the handle to the device context. We release it by calling the ReleaseHdc() method of the Graphics class.

Note that if we do not have an existing Graphics object, we can use the GDI function CreateDC() to obtain one. In that case, we will need to call another GDI function, ReleaseDC(), to indicate when we no longer need the device context in order to release the associated resources. Failure to do so won't just cause memory leaks-it can theoretically cause future drawing operations to crash because there are restrictions on the number of device contexts that can exist simultaneously, though precise details of this are not really documented. As long as you release device contexts when you are finished with them, you will be fine.

The Simple BitBltExample

The following example is a very simple example that uses the BitBlt function-its sole purpose is to demonstrate the use of the BitBlt() method, before we go on to use it properly in the AlphaDragExample example. In this example, we will draw a small figure in our form, then BitBlt it to another place in the window.

Create a new Visual C# Windows Application, calling it BitBltExample. We will add a constant to the Form1 class as a mnemonic for the SRCCOPY raster flag:

public class Form1 : System.Windows.Forms.Form
{
 const int SRCCOPY = 0xcc0020;

Add the DllImport attribute and the declaration of the external BitBlt() method to the Form1 class, as follows:

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern int BitBlt(
  IntPtr hdcDest,     // handle to destination DC (device context)
  int nXDest,         // x-coord of destination upper-left corner
  int nYDest,         // y-coord of destination upper-left corner
  int nWidth,         // width of destination rectangle
  int nHeight,        // height of destination rectangle
  IntPtr hdcSrc,      // handle to source DC
  int nXSrc,          // x-coordinate of source upper-left corner
  int nYSrc,          // y-coordinate of source upper-left corner
  System.Int32 dwRop  // raster operation code
  }:

Add a Paint event, and modify it as follows:

private void Form1_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e. Graphics;
  g.FillRectangle(Brushes.White, ClientRectangle);
  g.DrawRectangle(Pens.Black, 10, 10, 40, 40);
 IntPtr dc = g.GetHdc();
 BitBlt(dc, 70, 0, 60, 60, dc, 0, 0, SRCCOPY);
 // WinGdi.H contains constants for BitBlt
 // 0xCC0020 is the SRCCOPY raster operation
 g.ReleaseHdc(dc);
}

When we run the example, it appears as follows:

Click To expand


Drawing While Dragging

There is another reason why we want to draw during mouse events. When dragging and dropping some object from one part of our application to another, we may want to show what we are dragging. In this section, we will develop a code sample that illustrates how we can do that. Bear in mind when working through the sample that there are many ways you could implement this kind of feature-the sample shows one way to hopefully give you ideas that you can adapt in your own applications.

This sample will use the GDI BitBlt() function that we have just seen in action. We will need it because we will want to copy a portion of the display to a backing store-a bitmap held in memory.

This technique consists of using the BitBlt() functionality in a variety of ways. We can get a device context for the display. From the device context, we can create a Graphics object, so that we can use GDI+ to draw directly to the display. We then use BitBlt() function to save the contents under the cursor to a backing store. After we have saved a region of the display to a backing store, we can draw directly to the display, knowing that we can repair it using the backing store. This example draws using an alpha technique to the screen.

The AlphaDragExample

Create a new Windows Application and call it AlphaDragExample. Note that this example will use unsafe code blocks. This means that it won't compile unless you tell Visual Studio .NET to allow unsafe code. We will see how to do that before we look at the code for the sample.

Compiling with Unsafe Code

You modify a project so that it can compile when it contains unsafe code via the Project | Properties window. Select Configuration Properties, then Build in the tree view to the left of the window, and ensure that the drop-down listbox at the top of the dialog shows All Configurations. Then, locate the Allow unsafe code blocks option and set it to True:

Click To expand

Coding up Drawing While Dragging

Now back to the sample code. To start off, we will add a couple of namespace references:

using System;
using System. Drawing;
using System.Drawing.Drawing2D;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Data;

We need System.Drawing.Drawing2D because we will be using LinearGradientBrush, and System.Runtime.InteropServices to access the DllImport attribute.

Next, inside the Form1 class, add the following DllImport definitions:

[DllImport("gdi32.dll")]
private static extern bool BitBlt(
  IntPtr hdcDest,     // handle to destination DC (device context)
  int nXDest,         // x-coord of destination upper-left corner
  int nYDest,         // y-coord of destination upper-left corner
  int nWidth,         // width of destination rectangle
  int nHeight,        // height of destination rectangle
  IntPtr hdcSrc,      // handle to source DC
  int nXSrc,          // x-coordinate of source upper-left corner
  int nYSrc,          // y-coordinate of source upper-left corner
  System.Int32 dwRop  // raster operation code
  };
[DllImport("gdi32.dll")]
private static extern IntPtr CreateDC(
  String driverName,
  String deviceName,
  String output,
  IntPtr lpInitData
  };

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll"
private static extern bool DeleteDC(
  IntPtr dc
  };

[System.Runtime.InteropServices.DllImportAttribute("user32.dll")]
private static extern unsafe bool ClientToScreen(
  IntPtr hWnd,       // handle to window
  Point* lpPoint     // screen coordinates
  };

We will also need the following fields in our Form1 class:

const int SRCCOPY = 0xcc0020;
private Point dragMePosition;
private bool mouseDown = false;
private Point mouseDelta;
private Bitmap backingStore;
private Point backingStorePos;
private String dragString;
private Graphics backingStoreGraphics;
private IntPtr backingStoreDC;
private Graphics displayGraphics;
private IntPtr displayDC;
private Size dragImageSize;
private Font dragStringFont;

The purposes of these fields should be clear from their names. The backing store is the bitmap that will be stored behind the scenes containing the image to be dragged and there are various fields describing its size and contents, as well as a device context handle, represented in .NET by an IntPtr instance.

We initialize several of these fields in the constructor:

public Form1()
{
  InitializeComponent();
  dragString = "Drag Me!";
  dragMePosition = new Point(100, 100);
  dragStringFont = new Font("Times New Roman", 14, FontStyle.Bold);
}

Some initialization also takes place in the Load event handler, which you should add to the project. We calculate the size of the string to be dragged there rather than in the constructor, since we need to be able to access a Graphics object:

private void Form1_Load(object sender, System.EventArgs e)
{
  Graphics g = this.CreateGraphics();
  SizeF temp = g.MeasureString(this.dragString, this.dragStringFont);
  dragImageSize = new Size((int)temp.Width, (int)temp.Height);
  g.Dispose();
}

Next, we add a couple of helper methods to work with the GDI objects we will be using:

private void getDisplayDC()
{
  displayDC = CreateDC("DISPLAY", null, null, (System.IntPtr)null);
  displayGraphics = Graphics.FromHdc(displayDC);
}

private void disposeDisplayDC()
{
  displayGraphics.Dispose();
  DeleteDC(displayDC);
}

private unsafe Point controlToScreen(int x, int y)
{
   Point p = new Point(x, y);
   Point* pp = &p;
   ClientToScreen(Handle, pp);
   return p;
}

As you can see from this code, to get a device context for the display, and then to create a Graphics object, we do the following:

displayDC = CreateDC("DISPLAY", null, null, (System.IntPtr)null);
displayGraphics = Graphics.FromHdc(displayDC);

The device context is removed by calling DeleteDC().

The controlToScreen() method that we define uses the API function ClientToScreen(), which is GDI's way of converting from client coordinates (top-left corner of client area of control = (0, 0) to screen coordinates (top-left corner of screen = (0, 0). The function requires a pointer to a Point object. This requires that we use unsafe code to get such a pointer.

Now let's examine the Paint event handler:

private void Form1_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.FillRectangle(Brushes.Bisque, ClientRectangle);
  Font bf = new Font("Times New Roman", 32);
  g.DrawString("Bonjour Le Monde", bf, Brushes.Blue, 10, 10);
  bf.Dispose();
  drawDragImage(
    g, false, dragMePosition.X, dragMePosition.X, dragString);
}

This handler simply draws the Bonjour Le Monde text, then calls another method, drawDragImage(), which we will examine next, that draws the Drag Me text in its normal position. drawDragImage() looks like this:

private void drawDragImage(
  Graphics g, bool alphablend, int x, int y, string s)
{
  Rectangle r = new Rectangle(
    x, y, dragImageSize.Width, dragImageSize.Height);

  // Draw background.
  if (alphablend)
  {
    Color c1 = Color.FromArgb(0, Color.Red);
    Color c2 = Color.FromArgb(80, Color.Red);
    Rectangle r1 = new Rectangle(r.Left, r.Top, r.Width / 2, r.Height);
    Rectangle r2 = new Rectangle(
      r.Left + r.Width / 2, r.Top, r.Width / 2, r.Height);
    LinearGradientBrush lgb1 = new LinearGradientBrush(r1, c1, c2, 0f);
    LinearGradientBrush lgb2 = new LinearGradientBrush (r2, c2, c1, 0f);
    g.FillRectangle(
     lgb1, new Rectangle(r1.Left + 1, r1.Top, r1.Width + 1, r1.Height));
    g.FillRectangle(lgb2, r2);
    lgb1.Dispose();
    lgb2.Dispose();
  }
  else
    g.FillRectangle(Brushes.Red, r);

  // Draw the text.
  Color c;
  if (alphablend)
    c = Color.FromArgb(80, Color.Black);
  else
    c = Color.Black;
  Brush blackBrush = new SolidBrush(c);
  g.DrawString(s, this.dragStringFont, blackBrush, r);
  blackBrush.Dispose();
}

This method is more complex as it doesn't just have to cope with drawing the image in its normal location, but also with drawing it semi-transparent when it's being dragged, in which case the alphablend parameter passed to the method will be true. If alphablend is true, we construct a background rectangle using linear gradient brushes with colors of varying opacity, otherwise we just construct a solid red brush to paint the text on. Either way, this method basically constructs the various brushes needed and draws the text. Note that Color.FromArgb(), when passed an integer and a Color, gives a color with the color itself determined by the Color parameter and opacity determined by the integer. 0 gives a completely transparent image while 255 is completely opaque. Intermediate values will draw the image, but with the background showing through to some extent. Hence the code:

Color c2 = Color.FromArgb(80, Color.Red);

Will store a semi-transparent Red color in the local variable c2.

Now let's look at the three mouse-event handlers. We need handlers for the MouseDown, MouseMove, and MouseUp events. We will start with the code for MouseDown:

private void Form1_MouseDown(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  Rectangle dragMeRectangle = new Rectangle(
   dragMePosition, new Size(dragImageSize.Width, dragImageSize.Height));
  if (dragMeRectangle.Contains(new Point(e.X, e.Y)))
  {
    mouseDown = true;
    mouseDelta = new Point(
      e.X - dragMePosition.X, e.Y - dragMePosition.Y);

    Point screenPoint = controlToScreen(e.X, e.Y);
    screenPoint.X -= mouseDelta.X;
    screenPoint.Y -= mouseDelta.Y;
    getDisplayDC();

    backingStore =
      new Bitmap(dragImageSize.Width, dragImageSize.Height);
    backingStoreGraphics = Graphics.FromImage(backingStore);
    backingStoreDC = backingStoreGraphics.GetHdc();
    BitBlt(backingStoreDC, 0, 0, backingStore.Width,
      backingStore.Height, displayDC, screenPoint.X,
      screenPoint.Y, SRCCOPY);
    backingStorePos = new Point(screenPoint.X, screenPoint.Y);
    drawDragImage(
      displayGraphics, true, screenPoint.X, screenPoint.Y, dragString);
  }

We don't actually need to do anything unless the user has depressed the mouse button inside the text that can be dragged, so that's the first check we make. If that is the case, then we record the fact that we are now dragging, as well as a field called mouseDelta-this is the starting location of the mouse cursor relative to the top-left corner of the region that can be dragged. We will need this value to work out how far the mouse has moved and therefore where to draw the dragged image while dragging. The bulk of the remainder of this method is concerned with saving the contents of the screen in the dragging rectangle to a memory bitmap-the so-called backing store. Note that we need to use BitBlt() to do this, although we are able to stick with GDI+ for the remaining code.

Now let's examine the MouseMove event handler:

private void Form1_MouseMove(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  if (mouseDown)
  {

    Point screenPoint = controlToScreen(e.X, e.Y);
    screenPoint.X -= mouseDelta.X;
    screenPoint.Y -= mouseDelta.Y;
    BitBlt(displayDC, backingStorePos.X, backingStorePos.Y,
      backingStore.Width, backingStore.Height, backingStoreDC,
      0, 0, SRCCOPY);
    BitBlt(backingStoreDC, 0, 0, backingStore.Width,
      backingStore.Height, displayDC, screenPoint. X,
      screenPoint.Y, SRCCOPY);
    backingStorePos = new Point(screenPoint.X, screenPoint.Y);
    drawDragImage(
      displayGraphics, true, screenPoint.X, screenPoint.Y, dragString)
  }
}

This handler doesn't need to do anything unless the mouseDown field indicates that we are in the middle of a drag operation. If we are, then the screen requires updating. The two BitBlt() operations are to do with restoring the screen area where the semi-transparent image had been before the mouse moved, then backing up the screen again. Then, we call drawDragImage() to actually draw the dragged image in the new position.

The final method we need to consider is the event handler for the MouseUp event that ends the drag operation:

private void Form1_MouseUp(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  if (mouseDown)
  {
    mouseDown = false;
    BitBlt(displayDC, backingStorePos.X, backingStorePos.Y,
      backingStore.Width, backingStore.Height, backingStoreDC,
      0, 0, SRCCOPY);

    disposeDisplayDC();

    backingStoreGraphics.ReleaseHdc(backingStoreDC);
    backingStoreGraphics.Dispose();

    backingStore.Dispose();
    backingStore = null;
  }
}

This handler simply uses a BitBlt() to remove the dragged image from the screen. Then, it releases the temporary Graphics object and related HDC that we created on the MouseDown event for the drag operation.

This example just shows the bare basics of this technique. A more generalized version would be data-driven, could drag a variety of objects, and so on. However, there would not be anything technically different about a more generalized technique.

Using this technique, we can give our application a sophisticated look, and provide a greater level of feedback to the user.

Improving Drawing Performance

If you run the sample you will find that it works well, though there may be some flickering as you drag the control. Some flickering can be hard to avoid when we are dealing with moving graphical images. Although details of more advanced techniques to smooth out moving images are beyond the scope of this book, we will review a few of the points you should look out for:

  • Remove any unnecessary work from the drawing routines. In our AlphaDragExample there is some scope for that. For example, the drawDragImage() method works out the gradient brushes each time it is called. It would be more efficient to work out these brushes once and cache them.

  • The drawDragString() method in our example uses Graphics.DrawString() to draw the text. In general, drawing text is exceptionally slow. If the same text needs to be drawn multiple times, consider drawing it once to an in-memory bitmap, then BitBltting the bitmap instead whenever you need to draw the text.

  • If you are using GDI+, examine your code carefully for cases where you might be able to take advantage of GDI's lower-level control to arrange the code more efficiently.

  • Consider using a back buffer. .NET forms can use back buffers automatically-look up the ControlStyles.DoubleBuffer enumeration in the MSDN documentation for more details on this.

  • Finally, if the drawing is still too slow, you might want to consider moving to DirectX for your drawing code, though normally, you should only need this for very high performance applications such as games.


Drag-Scrolling

In this section, we will look at the question of implementing a control in which you want the user to be able to simultaneously scroll and select text by holding the cursor just outside the client area-as, for example, can be done in Word.

There is one more area of mouse-event processing that is quite often implemented incorrectly. This bug is most often found with a custom control that implements some form of vertical or horizontal scrolling. In its crudest form, this bug shows up when dragging and selecting text. When the user drags the mouse outside of the client area of the custom control, the control should start scrolling as it continues to select text. However, with some controls or applications, when the mouse moves outside of the client area, the client area scrolls, and then stops. When the user moves the mouse again, the client area scrolls again, until the user stops moving the mouse. In order to scroll continuously, the user needs to ‘wiggle' the mouse below or above the client area while holding the mouse button down. I call this the ‘wiggle-the-mouse-bug'.

To understand why this bug happens, we need to look at the algorithm used to implement scrolling while selecting. The algorithm consists of the following points:

  • Initially, as the user drags within the client area of the control, text is selected in the control.

  • When the user drags below or above the control, the MouseMove event handler scrolls the control, and then selects the appropriate text.

  • The quantity to scroll is best calculated based on the distance of the mouse above or below the client area of the custom control.

  • When implemented incorrectly, the control scrolls with each MouseMove event. This is the source of the bug.

What we need to do is to start a timer that generates mouse events, while the mouse is captured, at regular intervals. We then can scroll with each MouseMove event, and scrolling will be continuous even if we don't wiggle the mouse below the custom control.

However, this can lead to a variation on the above bug. If we trigger the scrolling on the MouseMove events that come from the mouse, and on the MouseMove events that we generate from the timer, we will end up with the bug that I call the ‘wiggle-the-mouse-to-scroll-faster-bug'. In other words, wiggling the mouse makes the scroll happen faster, contrary to user expectations. The solution to this is that when automatically scrolling, we only scroll based on the timer-generated MouseMove events. However, we can't completely ignore the mouse generated MouseMove events. We have to note the location of the mouse, and use that location in the generation of the timer-based MouseMove events.

Demonstrating the ‘Wiggle-the-Mouse Bug'

In this section we present an example that demonstrates the ‘wiggle-the-mouse bug'. This example consists of a custom control, similar to a ListBox control, which allows us to select rows with the mouse.

Click To expand

Although the contents of the control look a bit like a ListBox, it isn't in fact-the effect is created entirely by our own custom drawing code.

The control allows us to select rows with the mouse. While doing this, we can drag above or below the custom control, and the control will scroll as it continues to select rows. The farther above or below that we drag the mouse while scrolling, the faster the custom control scrolls. In the next section, we will show how to solve the bug.

This custom control is a variation on the smooth scrolling custom control presented in the previous chapter, so create a new Windows Control Library project and call it ScrollAndSelectList. Rename the C# class to ScrollAndSelectList.cs and, similarly, change its (Name) property as well. We need to modify the namespaces used in this example, at the top of the code file. This control will also be using the GraphicsOM assembly so add a namespace reference and a reference to the project:

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using System.Threading;
using Wrox.GraphicsOutlineModel;

In this case, we can leave the control as derived from UserControl. Use the Toolbox to add a VScrollBar to it, call it vScrollBar, and set its Dock property to Right.

Now add the following fields to the class:

private Point originOfImage = new Point(0, 0);
private Size virSpaceSize = new Size(100, 1000);
private Bitmap bitmap;
private int startingSelectedRow = −1;
private int endingSelectedRow = −1;
private int lineSpacing;
private int nbrRows;
private bool mouseDown = false;
private int draggingStartedRow;
private int old_startingSelectedRow = −1;
private int old_endingSelectedRow = −1;

We also add a line to the constructor for the Form1 class:

public ScrollAndSelectList()
{
  // This call is required by the Windows.Forms Form Designer.
  InitializeComponent();

  this.SetStyle(Control Styles.Opaque, true);
}

Setting the control style to Opaque prevents the background being repainted when the control is painted. This is important to prevent flickering while we are dragging the mouse to select rows.

Now we will do some housekeeping. The control will work by drawing from a large bitmap the size of the virtual space-that is, big enough to display all rows in the ‘listbox'. The technique we will be using is to just display the appropriate portion of this bitmap on the screen using Graphics.DrawImage(). The size of this space is accessed via the VirtualSpaceSize property:

public Size VirtualSpaceSize
{
  get
  {
    return virSpaceSize;
  }
  set
  {
    // Adjust size so that there are an integral number of rows.
    lineSpacing = (int)Font.Height;
    virSpaceSize = value;
    nbrRows = virSpaceSize.Height / lineSpacing;
    virSpaceSize.Height = nbrRows * lineSpacing + 1;
    bitmap = new Bitmap(
      this.virSpaceSize.Width, this.virSpaceSize.Height);
    drawIntoImage();
    Invalidate();
    adjustScrollBars();
    return new Rectangle(0, 0, this.vScrollBar.Left,
  }
}

As you can see, setting this property causes a new bitmap to be allocated, followed by calls to a couple of methods that we will code up soon, drawIntoImage() actually draws the listbox into the bitmap. Then, we call Invalidate() to raise a Paint event and get the bitmap drawn to the screen. Finally, adjustScrollBars() sets up the scrollbars based on the size of the virtual space.

We also need to make sure the bitmap resource is cleaned up so we modify the Dispose() call:

protected override void Dispose(bool disposing)
{
  bitmap.Dispose();
  if( disposing )
  {
    if( components != null )
      components.Dispose();
  }
  base.Dispose( disposing );
}

Next, we define a property that gives us the actual drawing area-that is, the area of the control that is not occupied by the scrollbar:

private Rectangle ScrollingImageRectangle
{
  get
  {
    return new Rectangle(0, 0, this.vScroflBas.Left,
    this.Height);
 }
}

The adjustScrollBars() method sets the scrollbars up with the data they need to display correctly, given the current position of the control we are looking at:

private void adjustScrollBars()
{
  vScrollBar.Minimum = 0;
  vScrollBar.Maximum = this.virSpaceSize.Height - 2;
  vScrollBar.LargeChange = ScrollingImageRectangle.Height - 1;
  vScrollBar.SmallChange = lineSpacing;
  vScrollBar.Value = originOfImage.Y;
}

The drawIntoImage() method actually draws the bitmap we will be displaying. It simply iterates through the rows, writing out some text in each one, as well as drawing the lines that separate the rows:

private void drawIntoImage()
{
  int y;

  Graphics g = Graphics.FromImage(bitmap);
  g.FillRectangle(Brushes.White, new Rectangle(
    0, 0, bitmap.Width, bitmap.Height));
  GraphicsOM g2 = new GraphicsOM(g);
  for (int i = 0; i < nbrRows; ++i)
  {
    y = i * lineSpacing;
    g2.DrawLine(Pens.Black, 0, y, virSpaceSize.Width, y);
    Rectangle r = new Rectangle(
      0, y, virSpaceSize.Width, lineSpacing - 1);
    if (i >= startingSelectedRow && i <= endingSelectedRow)
      g2.FillRectangle(Brushes.LightBlue, new Rectangle(
        r.Left, r.Top + 1, r. Width, r. Height));
    g.DrawString("line: " + i, Font, Brushes.Black, r);
  }
  y = lineSpacing * nbrRows;
  g2.DrawLine(Pens.Black, 0, y, virSpaceSize. Width, y);
  g.Dispose();
}

Now let's consider the Paint event handler. This creates the backing bitmap, if necessary, before drawing using a helper method, drawImage(). We will consider both methods together:

private void ScrollAndSelectList_Paint(
  object sender, System.Windows.Forms.PaintEventArgs e)
{
  Graphics g = e.Graphics;
  if (bitmap != null)
  {
    lineSpacing = (int)Font.Height;
    nbrRows = virSpaceSize.Height / lineSpacing;
    drawIntoImage();
    drawImage(g, this.originOfImage.X, this.originOfImage.Y);
  }
  else
  {
    lineSpacing = (int)Font.Height;
    nbrRows = virSpaceSize.Height / lineSpacing;
    bitmap = new Bitmap(virSpaceSize.Width,
      virSpaceSize.Height);
    drawIntoImage();
    adjustScrollBars();
  }
}

private void drawImage(Graphics g, int x, int y)
{
  if (bitmap != null)
  {
    g.SetClip(ScrollingImageRectangle);
    Rectangle drawRect = new Rectangle(new Point(x, y),
      ScrollingImageRectangle.Size);
    g.DrawImage(bitmap, ScrollingImageRectangle,
      drawRect, GraphicsUnit.Pixel);
  }
}

One more housekeeping method, getRowFromHitTest() examines the mouse position and determines what row this is in. Since this method will be called by the mouse event handlers we write, it takes the mouse position from a MouseEventArgs parameter:

private int getRowFromHitTest(System.Windows.Forms.MouseEventArgs e)
{
  Point newPoint = new Point(e.X, e.Y + originOfImage.Y);
  int row = newPoint.Y / lineSpacing;
  return row;
}

Now we will get on to the part of the code relating to scroll-selecting. First, the event handler for MouseDown. This code simply checks what row the mouse was pressed in, sets some variables to indicate we are now in a drag-select operation, and redraws the current row selected:

private void ScrollAndSelectList_MouseDown(object sender,
  System.Windows.Forms.MouseEventArgs e)
{
  int row = getRowFromHitTest(e);
  this.startingSelectedRow = row;
  this.endingSelectedRow = row;
  draggingStartedRow = row;
  drawIntoImage();
  this.Invalidate();
  mouseDown = true;
}

The next stage in drag-selecting is when the user moves the mouse. Here's the event handler for the MouseMove event:

private void ScrollAndSelectList_MouseMove(object sender,
  System.Windows.Forms.MouseEventArgs e)
{
  processMouseMove(e);
}

As you can see, the MouseMove handling has been placed in a separate method, processMouseMove(). At the moment, that extra method call looks redundant, but it will come in handy later on when we fix the wiggle-the-mouse bug, which will involve adding some more code to ScrollAndSelectList_MouseMove(). The processMouseMove() method looks like this:

private void processMouseMove(System.Windows.Forms.MouseEventArgs e)
{
  if (mouseDown)
  {
    if (e.Y >= ScrollingImageRectangle.Size.Height)
    {
      int delta = e.Y - ScrollingImageRectangle.Size.Height;
      delta = (delta / lineSpacing + 1) * lineSpacing;
      Point oldOrigin = originOfImage;
      originOfImage = new Point(
        originOfImage.X, Math.Min(
          bitmap.Height - ScrollingImageRectangle.Size.Height,
          oldOrigin.Y + delta));
      slide(oldOrigin, originOfImage);
    }
    if (e.Y < 0)
    {
      int delta = −e.Y;
      delta = (delta / lineSpacing + 1) * lineSpacing;
      Point oldOrigin = originOfImage;
      originOfImage = new Point(
        originOfImage.X, Math.Max(0, oldOrigin.Y - delta));
      slide(oldOrigin, originOfImage);
    }
    adjustScrollBars();

    int row = getRowFromHitTest(e);
    if (row < draggingStartedRow)
    {
      this.startingSelectedRow = row;
      this.endingSelectedRow = draggingStartedRow;
    }
    else
    {
      this.startingSelectedRow = draggingStartedRow;
      this.endingSelectedRow = row;
    }
    if (old_startingSelectedRow !=
      startingSelectedRow | | old_endingSelectedRow != endingSelectedRow)
    {
      drawIntoImage();
      this.Invalidate();
    }
    old_startingSelectedRow = startingSelectedRow;
    old_endingSelectedRow = endingSelectedRow;
  }
}

The first part of this method is the key part of this sample. If the mouse has moved to above or below the client area of the control, then we discover this by examining the e.Y value. In this case, we slide the scrollbars by an appropriate amount using a slide() method that we'll examine soon.

Then, the bulk of the code for the MouseMove event handler is devoted to modifying the recorded set of selected rows-as indicated by the startingSelectedRow and endingSelectedRow fields, and repainting to ensure all those rows are drawn selected. Notice how the old_startingSelectedRow and old_endingSelectedRow fields are also used to temporarily store the range of selected rows for comparison the next time that a MouseMove event occurs so we can look for any changes as a result of moving the mouse.

Now for the final part of the selection process-the point where the user releases the mouse button. Here, we simply set some fields to indicate that the drag-select operation is over:

private void ScrollAndSelectList_MouseUp(object sender,
  System.Windows.Forms.MouseEventArgs e)
{
  mouseDown = false;
  old_startingSelectedRow = −1;
  old_endingSelectedRow = −1;
}

Before we go on to examine the MouseUp event handler, let's look at that slide() method, which is designed to slowly scroll the control. Since the emphasis is on scrolling slowly, we use the Thread.Sleep() method to wait for 10 milliseconds at the end of the process to slow the scrolling down:

private void slide(Point oldP, Point newP)
{
  Graphics g = this.CreateGraphics();

  if (oldP.Y != newP.Y)
  {
    // Slide vertically.
    bool goingUp = newP.Y - oldP.Y > 0;
    int delta = (newP.Y - oldP.Y) / 12;
    if (goingUp && delta < 1)
      delta = 1;
    if (!goingUp && delta > −1)
      delta = −1;
    int i = oldP.Y;
    while (true)
    {
      if (i == newP.Y)
        break;
      i += delta;
      if (goingUp && i > newP.Y)
      {
        i = newP.Y;
        continue;
      }
      if (!goingUp && i < newP.Y)
      {
        i = newP.Y;
        continue;
      }
      drawImage(g, oldP.X, i);
      if (i != newP.Y)
        Thread.Sleep(10);
    }
  }
}

Finally, we also need an event handler for when the user directly scrolls using the vertical scrollbar. This method simply examines the new position of the scrollbar and adjusts the part of the listbox that is being viewed accordingly:

private void vScrollBar_Scroll(
  object sender, System.Windows.Forms.ScrollEventArgs e)
{
  switch (e.Type)
  {
    case ScrollEventType.SmallIncrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.LargeIncrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.SmallDecrement:
      goto case ScrollEventType.LargeDecrement;
    case ScrollEventType.LargeDecrement:
      Point oldOrigin = originOfImage;
      originOfImage = new Point(originOfImage.X, e.NewValue);
      slide(oldOrigin, originOfImage);
      break;
    default:
      originOfImage = new Point(originOfImage.X, e.NewValue);
      this.Invalidate();
      break;
  }
}

That completes the control. We now require a test form to host it in and test it, so add a Windows Application to the current project and call it TestScrollAndSelectList. Set it as the startup project, add a reference in the test application to the custom control, and on its form, place a ScrollAndSelectList custom control. Change the font for the control to a larger setting, say a Times New Roman, 14 pt. Also, the application will look better if you set the Dock property to Fill.

Run the program, select a row, and drag the mouse below the control. You will notice that the control scrolls so long as we wiggle the mouse. Now, let's modify the custom control and eliminate this bug.

Removing the Wiggle Bug

To remove the bug, add the following private member variables to the ScrollAndSelectList custom control:

private bool autoDrag = false;
private Point lastMousePoint;
private System.Windows.Forms.Timer timer;

As you can see, we have a timer to measure how long the user holds the mouse outside the client area, as well as a bool to indicate if we are in autoDrag mode (in other words, the user is selecting by holding the mouse outside the client area) and a Point field to store the last position of the mouse in this situation.

Next, modify the MouseMove event handler so that it can detect if the user has moved the mouse outside the client area, in which case, autoDrag needs to commence (or be stopped if the user has moved the mouse back into the client area):

private void ScrollAndSelectList_MouseMove(
  object sender, System.Windows.Forms.MouseEventArgs e)
{
  if (mouseDown)
  {
    if (e.Y >= ScrollingImageRectangle.Size.Height || e.Y < 0)
    {
      lastMousePoint = new Point(e.X, e.Y);
      if (!autoDrag)
        startAutoDrag();
    }
    else
    {
      if (autoDrag)
        stopAutoDrag();
      processMouseMove(e);
    }
  }
  else
    processMouseMove(e);
}

Note that the MouseMove event handler passes its event on to the processMouseMove method only if the control is not auto-dragging.

Here are the startAutoDrag() and stopAutoDrag() methods that deal with starting and stopping the timer:

private void startAutoDrag()
{
  autoDrag = true;
  timer = new System.Windows.Forms.Timer();
  timer.Interval = 100;
  timer.Tick += new EventHandler(tickEventHandler);
  timer.Start();
}

private void stopAutoDrag()
{
  autoDrag = false;
  if (timer != null)
  {
    timer.Stop();
    timer.Dispose();
    timer as null;
  }
}

Here is the handler for the Tick event in the timer, which simulates the mouse being moved by calling the processMouseMove() method to add some more rows to the selection:

private void tickEventHandler(Object o,
  EventArgs e)
{
  MouseEventArgs mea = new MouseEventArgs(MouseButtons.Left,
    1, lastMousePoint.X, lastMousePoint.Y, 0);
  processMouseMove(mea);
}

We must also modify the MouseUp event handler, adding a call to the stopAutoDrag() method:

private void ScrollAndSelectList_MouseUp(object sender,
  System.Windows.Forms.MouseEventArgs e)
{
  mouseDown = false;
  old_startingSelectedRow = −1;
  old_endingSelectedRow = −1;
  StopAutoDrag();
}

Now, recompile and rebuild the application. The application no longer exhibits the ‘wiggle-the-mouse-bug'.


Summary

In this chapter, we examined some of the more advanced problems associated with processing mouse events.

We looked at using mouse cursors to give a greater level of feedback to the end user. We can use the stock cursors, or we can create custom cursors.

Hit testing is made easy by functionality in the Rectangle structure, and the Region and GraphicsPath classes.

In this chapter, we saw the difference between drawing during a Paint event and drawing during a mouse event; we can get a Graphics object for the form during the mouse event, and draw without waiting for a Paint event. We also saw how we can draw directly to the display using an alpha technique, creating a semi-transparent image of the object being dragged.

We also examined the preventative measures we need to take so that we don't have the situation where our control thinks that the mouse button is down when it is not.

Finally we saw how to code up an application that allows the user to select parts of the display area by holding the mouse just outside the client area in a similar manner to many text editors.


Chapter 15: GDI+ Images in ASP.NET

Overview

As we have seen in the preceding chapters, C# and .NET provide real improvements over previous technologies and make building great graphical desktop applications a much less painful experience than before. This is fine if you want to develop for the desktop, but what about the Web?

There are quite a few situations where GDI+ can come in really handy for enhancing your web application. You might want to have a graphical hit-counter, an electronic postcard, a graph or chart, or maybe place a caption on a photograph captured with a webcam.

Until now, we have used GDI+ primarily with Windows Forms. We have written Windows Forms custom controls, or used GDI+ in the Paint event handler for a Form object. However, with the .NET Framework, we can use GDI+ in a variety of other contexts:

  • We can generate images using GDI+, and serve them up from a web server

  • We can build ASP.NET custom controls, and integrate images into them generated using GDI+

  • We can build Web Services that build images using GDI+, and return them to the consumer of the Web Service

Having a unified application-programming interface for graphical operations for Windows Forms, Windows Forms custom controls, and all of the above contexts is one of the important features of the .NET Framework. With ASP (the predecessor to ASP.NET), using GDI (the predecessor to GDI+) was cumbersome. One approach was to write a COM object that we could call from the ASP code. In contrast, the ease with which we can use GDI+ in ASP.NET is a breath of fresh air.

The examples that we will present in the final three chapters are not intended to be comprehensive examples of GDI+. Instead, they provide a framework for using GDI+ in different contexts. Of course, we can bring the full power of GDI+ to bear in these situations, creating a wonderful level of functionality. I can envision serving very elaborate graphics as part of a web site, generated from sophisticated databases. Building ASP.NET custom controls using GDI+ has a huge potential. This topic could fill half a book by itself-I can imagine an aircraft manufacturer creating lifelike images of the control panel of a plane, and allowing the user to ‘operate' the instruments of the simulated aircraft using a web browser. Or, a manufacturer of a complicated instrument such as a Global Positioning System (GPS) presenting a simulated GPS for training and demonstration purposes on their web site. As an exercise after reading this book, one possible small project is to use GDI+ to make a fully working web simulation of the new computerized control panels of the NASA space shuttle (of course, getting the specifications of their operation may be a bit difficult!). I can't even imagine the variety of ways that builders of web services will find to use GDI+. Many images have a high intrinsic value, and a web service that generates saleable images will be a money machine.

So, when reading the following chapters, we don't use the most esoteric features of GDI+. We intentionally keep the examples simple. For instance, when building an ASP.NET custom control, we draw a rectangle in an image, and then let the user know when they have clicked on the rectangle in the image in the web browser. Extrapolating from this small example to the implementation of the space shuttle instrumentation is just a simple matter of generalization and software engineering!!

Our main goal in these three chapters is to show the use of GDI+ in different contexts, exploring how ASP.NET provides a rich platform for building web applications and enables a C# programmer to access GDI+ functionality to create and deliver graphics over the Internet. We will be concentrating mainly on the most popular web image file formats, GIF and JPEG, but, as previously explained in earlier chapters, GDI+ can work with many other image file formats, including BMP, TIF, and WMF. It is not necessary to demonstrate again the more advanced features of GDI+, as these have been covered earlier on in the book. Rest assured that, if you wish, you can use these advanced features in the contexts we are about to explore.

First, we will load images into memory and serve them up to the user's web browser over the Internet. After this, we will take a look at how this works before investigating the two methods of delivering both custom images and HTML at the same time.

When we have a working web page, we will extend the code to enable us first to modify existing images with flips, resizes, captions, and rotations, and then to create custom images on the fly, serving up bitmaps created purely in code.

Before delving into the GDI+ with ASP.NET examples, though, we first need to have some details on setting up Microsoft's Internet Information Services (IIS), which is the web server we will be using for our examples.


Internet Information Services (IIS)

If you already have Microsoft Internet Information Services (IIS) installed and running on your machine, feel free to skip this section of the chapter. For the benefit of those who haven't, we will quickly run through the installation process.

The procedures outlined here are for Windows 2000 Professional, as both Windows 2000 Server and Windows 2000 Advanced have IIS installed by default.

Bear in mind that the procedures presented here may change slightly based on the exact version of .NET and the operating system that you are running.

To install IIS, follow these steps:

  • Open up the Control Panel and click on Add/Remove Programs

  • Choose Add/Remove Components from the left-hand menu bar and you will see the Windows Components Wizard:

    Click To expand

  • If it is not already checked, make sure the Internet Information Services box is checked

  • Hit the Next button and follow the on-screen prompts to install IIS

If the installation was successful, you should be able to test it by going to the URL http://machinename , replacing machinename with local host or the server name of the machine on which you installed IIS. What you will see is a standard page for the operating system you are working on. In this case, we are using Windows 2000, so the current default page looks like the following: +

Click To expand

In order to manage IIS, Microsoft provides a snap-in called Internet Services Manager and this can be found under Control Panel | Administrative Tools. From this console, you will be able to start and stop services as you wish. We will be looking in more detail at the features this manager provides in a moment.

Documentation

If you have not read the IIS documentation then it is very worthwhile doing so as it is a very useful resource. To view the documentation after installing IIS, go to the Internet Services Manager snap-in just described and press F1 to bring up the help screens. The right-hand panel contains a link to the product documentation for IIS, so click on the link and the information will be displayed in a new browser.

Alternatively, since you have IIS installed and running, you can go directly to the documentation for IIS. If you followed the previous instructions and got to the documentation via the Internet Services Manager snap-in, you will have clicked on a link to access the information. Instead of this rather inefficient method, you can enter the URL http://machinenamee/iisHelp, where again, machinename is either localhost or the name of the machine you are working on.

Security and Virtual Directories

If your web server will be accessible outside of your development environment, be sure to tighten up the security permissions on your machine. To begin setting permissions, go to your Internet Services Manager, right-click on Default Web Site, and select All Tasks | Permissions Wizard from the menu and follow the on-screen prompts.

When asked whether you wish to inherit properties or select new settings, pick the first option. Next, pick the recommended option of replacing all directory and file permissions and click on Next to view the settings that are to be applied. Click on Next once again and finally Finish to apply the changes. This procedure effectively ‘resets' the permissions for an anonymous user accessing your web site.

Microsoft makes patches and tools available to keep your web server up to date with security issues. Make sure you check this resource regularly, at http://msdn.microsoft.com, and apply the latest updates.

In these chapters of the book, we will simply be creating and running web applications from the web root of the IIS server. By default, this means that solutions are saved and run in the path C:\Inetpub\wwwroot. It is possible to store the examples in a directory elsewhere and have IIS use an alias or virtual root to get access to them. We shall see how to do this now.

Let's say we create a new folder with the path C:\GDIPlus on our web server, which will contain all the code we will be creating in the following chapters. We can create a virtual directory that points to this physical file folder by doing the following:

  • Go to the Internet Services Manager MMC snap-in, right-click on Default Web Site, and select New | Virtual Directory.

  • Follow the instructions in the Virtual Directory Creation Wizard to create an alias for your physical directory. Alias is the name by which the directory will be identified in HTTP requests. In this case, we can specify GDIPlus as the alias and C:\GDIPlus as the path, which we can locate using the file browser.

    This means that when an HTTP request comes in with the URI http://machinename/GDIPlus/filename , where machinename is the name of the server on which the web server is running, IIS will respond to the request by looking for a file under C:\GDIPlus\filename.

  • Access permissions should be set to give at least Read access to clients for this virtual directory. For security reasons, you probably don't want to give any other permissions, though depending on the other files you wish to make available, you may want to give script execution permission too. These two are the default values anyway.

  • Finally, click on Finish to create the virtual directory.

    As with any operating system, and any web server, there are many opportunities to create security holes. As an example, in the last procedure, if we had set Access Permissions to Write, and had set application permissions to Run and Execute (to include standalone applications as well as scripts), we would have just granted anybody out on the Web the ability to upload any program that they care to, and then execute that program on our web server. This program could do anything that it wanted to, including format our hard disk or get confidential files on our server and expose them to the world. Web security is a big subject, and can't be covered in detail in this book, but be careful, and do enough research so that you are reasonably confident that you have created a secure system before going live with a web server.

    Now that we have the basics of IIS under our belt, we can move onto the GDI+ and ASP.NET examples.


Reading and Serving Images

As usual, we will start with the simplest example and work our way up to the more complex projects. The first thing you are going to want to do with graphics in a web application is to deliver a bitmap to the user's web browser. While this is a very simple operation, and at this stage not particularly impressive, it will allow us to investigate some of the principles and techniques that will help us later on when we take a look at some more adventurous applications.

In this example, we will simply read an image from the web server's file system and output the image to the user's browser. This is admittedly very easy to do using only HTML, but the difference here is that we are going to use C# code and ASP.NET to deliver the image.

In Visual Studio .NET, create a new ASP.NET Web Application. The IDE will create a new web form file for you called WebForm1.aspx. This file can be renamed by right-clicking on it in the Solution Explorer window. Go into the HTML view and delete any code that has been created for you by the environment. Now replace it with the following:

<%@ Page language="C#" Debug="true" %>
<Script runat="server">

private void Page_Load(object sender, System.EventArgs e)
{

  // Initialize objects.
  String Filename;
  System.Drawing.Image g;

  // Set the filename to a file in the local directory.
  Fi1ename = server.MapPath("./doggy.jpg");

  // Create a new image in memory from file.
  g = System.Drawing.Image.FromFile(Filename);

  // Set the mime type to JPEG.
  Response.ContentType="image/jpeg";

  // Send the image to the viewer.
  g.Save(Response.OutputStream, g.RawFormat);
  // clean up
  g.Dispose();

}

</Script>

You will need to replace the image reference, doggy.jpg, with a JPEG image of your own. Copy the image file to the same directory as your .aspx file. Files can be added to your application by right-clicking the application name in the Solution Explorer and selecting the option to add existing items. When this script is viewed (Build and Browse from the File menu, or Ctrl-F8) you should see the following (assuming that you have an identical dog and image!):

Click To expand

Hopefully, this example will not seem too difficult to follow after having worked through the examples in this book. Due to the fact this is an ASP.NET page serving an image potentially over the Internet via HTTP (although in this case we are viewing it locally) we need to approach things a little differently from if we were trying to achieve the same effect in a desktop application.

Let's have a look at the code in a little more detail. The first line of code tells the system this is page is using the C# language and that in this instance, we have debugging switched on:

<%@ Page Language="C#" Debug="true" %>

In the code, we point to our source image using'./ to signify that the file is in the same directory as the current application. You can tell the difference between a web path and a Windows file system path by the slashes that they use; web paths follow the UNIX convention of a forward slash while Windows uses the old DOS convention of backwards slashes. We are loading a file from the web server's file system, so we need the actual file path of the file rather than a relative web URL. We could have hardcoded a path, such as C:\myfile.jpg, but converting a relative path allows more flexibility when deploying to another server, as we do not need to worry that the directory structures outside of our application are different (as an example, web files could be stored on drive C: on the development machine and D: on the public web server). To convert our relative path to a file system path we use the Server.MapPath method:

Filename = Server.MapPath("./doggy.jpg");

Once we have a file name, we can construct our Image object from the bitmap file:

g = System.Drawing.Image.FromFile(Filename);

In the next line of code, we set the ContentType of the page to inform the web browser that the data it is to receive will be a JPEG image. If we did not set this, the browser may get confused, as by default, the browser will be expecting HTML text.

Response.ContentType = "image/jpeg";

This method requires a string that matches a correct MIME type, such as image/gif. In this case, as we are sending a JPEG, we use image/jpeg. Be sure not to make the popular mistake of using image/jpg.

Image files are effectively binary data in a specific format. To deliver this data to our user's web browser we must send it over a stream using the OutputStream property of the Response object. We write the image data to the stream using the Save() method of the Image class, to which we supply the stream to write out to, followed by the image format, which we supply from the RawFormat property of the Image class.

g.Save(Response.OutputStream, g.RawFormat);

The last operation, as always, is to clean up the objects that we have created in the page. This is good practice anyway, but is essential here since the objects that we created contain image data, which can eat up a lot of your memory-especially if many simultaneous users visit your page:

g.Dispose();

Streams

In the above example, we had to use a Stream object to serve the image data up to the user's web browser. This is because we want to deliver binary data. A Stream is simply an object that we can use to transfer data from one place or memory location to another. The data in question could be in memory, in a file, or sent over a network and in most cases these situations can be used interchangeably with only minor changes to your code.

This idea of a Stream is not a particularly new one, but is a very powerful concept all the same. One good use of a Stream is for converting your information from one format to another, for example to convert a 32 bit per pixel image into a bitmap with 8 bits per pixel, or as in the example we shall see in Chapter 17, returning an image from a web service as an array of bytes.

In the following examples we will see how a Stream is used to pull an image from a network resource in the form of a web URL, and later, we will look at writing to a file on the server's hard disk.

Reading an Image from a Remote Web Server

In these days of XML news syndication, live video feeds, and web services, it is not unusual for a web page to be composed of content sourced from many different web servers. With this in mind, it is clear that we might not be storing all our graphics on the server that will run our web application. Additionally, we might be creating a service whereby users will supply the images themselves, so we will not know in advance the location or format of the image to be used. Fortunately, ASP.NET has access to a rich collection of classes that are ideal for dealing with this scenario and make it very easy to access files on remote web servers.

In the following example, we will grab an image from a remote web site and deliver it to the user's web browser as if it were served from a local file. This example demonstrates a couple of new concepts, mainly that the source data for your GDI+ operations does not necessarily need to reside on the local file system and also how to detect the image format of the picture read.

Add a new Web Form item to your Visual Studio .NET web application accepting the default name of WebForm2.aspx. As before, select all the existing HTML code and delete it so that we can enter the following example code into a blank script. Since we are taking a picture from a remote web site, you will obviously need to have an active internet connection to run this example.

<%@ Page language="C#" Debug="true" %>
<%@ Import Namespace = "System.Net" %>
<%@ Import Namespace = "System.Drawing" %>
<%& Import Namespace = "System.Drawing.Imaging" %>

<Script runat="server">

private void Page_Load(object sender, System.EventArgs e)
{

   // Initialize the web object
   WebClient webC = new WebClient();

   // Set the URL of the file.
   string strFilename;
   strFilename="http://195.14.70.110/-wvhttp-01-/GetStillImage";

   // Create a bitmap based on the image from the URL.
   Bitmap b = new Bitmap(webC.OpenRead(strFilename));

   // If the image is GIF, set the content type to GIF,
   // otherwise use JPEG.
   if (b.RawFormat.Equals(ImageFormat.Gif))
   {
     Response.ContentType="image/gif";
   }
   else
   {
     Response.ContentType="image/jpeg";
   }
  // Send the image to the viewer.
  b.Save(Response.OutputStream, b.RawFormat)

  // Clean up.
  b.Dispose();
  webC.Dispose();

}

</Script>

For this example, we have connected to a webcam in London found at random by doing a Google search. In order that we do not upset the site owner by overloading this server, it would be best to find another image source when trying the code out for yourself! After you have entered this code and saved it, browse the example page and you should see a browser window complete with your image from the URL you specified. Here is what the code returns to the browser in our example using a London webcam:

Click To expand

In this example, we used the WebClient class to connect to a remote web server over the Internet using the HTTP protocol, and streamed a picture into memory before delivering it to the user.

WebClient is one of the many .NET classes that perform extremely powerful operations while being easy to use. Its job is to download and upload data to and from remote web servers, much like a web browser such as Internet Explorer. When downloading, you can either tell the WebClient to download to a file, or as we did in our example, read into memory for further processing.

As we did not necessarily know what image format we would be accessing, a check needs to be made on the image's RawFormat property to determine what ContentType to set for the page. Obviously, there are many image formats we can check for, but to keep the code simple, we only detect the two most popular (GIF and JPEG). After this, all we need to do is stream the image to the user as before.


Mixing HTML and Dynamic Images

In the preceding examples, we have output the image to the web browser directly. Although this is a perfectly valid way of working, this approach is quite limiting. When creating a web page, ordinarily, we would tell a browser to display a certain image by referencing it in the HTML as a file path in the src attribute of the img tag.

For example, the simplest web page that displays an image could be created as follows:

<html>

<head>
<title>Example HTML page</title>
</head>

<body>

An image:
<img src="image.gif">

</body>

</html>

The above example, when delivered to a web browser, would instruct the browser to go back to the server and request the image image.gif, which would then be displayed after the caption An image:. Of course, we see this kind of HTML markup all the time, but it requires that we know the filename of the image beforehand.

There are two ways we can approach building web pages that display dynamically created images. The first is simply to separate the HTML building page and the image creation script into two separate files, with the HTML referencing your image building script as a URL. This script is called WebForm3.aspx and calls the ASP.NET page, WebForm2.aspx, that took a picture from the webcam in London:

<html>

<head>
<title>GDI+</title>
</head>

<body>
What is happening in London?<br>
<img src

</body>

</html>

This is the result from the code:

Click To expand

This approach has the advantage that no files are written to the file system, meaning that you do not have any cleanup of temporary files, and security settings do not need to be tweaked to allow the user under whom the web page is running to create or modify files.

The second approach enables you to deliver dynamically created images to the user along with HTML code, all within one ASP.NET page. In this technique, we create and save a bitmap file with a known name, and then reference it in the HTML. We will explore this method in the following example, so add a new Web Form item to the solution, accepting it's default name of WebForm4.aspX, and enter the following code:

<%@ Page language="c#" %>
<%@ Import Namespace = "System. Net' %>
<%@ Import Namespace = "System. Drawing' %>
<%@ Import Namespace = "System. Drawing. Imaging" %>

<Script runat="server">

private void Page_Load(object sender, System.EventArgs e)
{

  // Initialize the web object.
  WebClient webC = new WebClient();

  // Set the URL of the file.
  string strFilename;
  strFilename="http://195.14.70.110/-wvhttp-01-/GetStillImage"
  // Create a bitmap based on the image from the URL.
  Bitmap b = new Bitmap(webC.OpenRead(strFilename));

  // Save the new image as a JPEG.
  String outFile = Server.MapPath("./image.jpg");
  b.Save(outFile, b.RawFormat);

  // Clean up.
  b.Dispose();
  webC.Dispose();

}

</Script>

<html>

<head>
<title>GDI+</title>
</head>

<body>

What is happening in London?<br>
<img src="image.jpg">

</body>

</html>

To make sure we do not have any problems writing to the image.jpg file, we need to ensure correct security permissions are set. Copy the image file you used in the first example, and call the new file image.jpg. Then, in Windows Explorer, right-click the file and select Properties. Now select the Security tab and for the aspnet_wp user, allow Modify, Read & Execute, Read, and Write by ticking the appropriate boxes.

The result of this example is exactly the same as WebForm4.aspx, with the obvious exception of the image returned by the webcam in London.

What we have done here is made the decision that the output filename will always be the same. For cases where the image would be uniquely created for each user, we need a system that builds a new image every hit. If we wish to stick with our ‘create a file then display it' system, then we could create the file based on the user's login, an incrementally or randomly created filename, or use the user's session ID.


Modifying Images

Before we go on to create an image from scratch, we should look at how we can do some common web image editing jobs on the fly, in C# code, which would ordinarily require someone to perform them manually in an image editing package such as Adobe Photoshop.

One operation that most web designers will have had to perform at one time or another is to create a thumbnail image. Thumbnails are smaller versions of large images that allow the site visitor to see a more quickly downloadable representation of the full picture, sometimes with a link to the larger version. You will see this a lot on e-commerce sites, such as Amazon.com, where you can see a small picture of a book cover and click to see a larger image that shows more detail.

It is a relatively easy task to load an image and then create a thumbnail manually, under a new name. This is OK if you only have a few images that don't change very often, but is not efficient in other situations, such as when you have images that:

  • Change on a more regular basis

  • Are submitted outside of your control or without your knowledge

  • Come from a live webcam feed

  • Are supplied by non-technical staff or users

As such, if you have any choice, it would be wiser and more cost-effective in terms of staff time to automate the process. The next example is very simple, as it only exists to illustrate this idea, but it does show how an image can be modified on the fly.

Start a new ASP.NET Web Application and call it like PictureView. Replace the HTML code with the following:

<%@ Page language="c#" %>

<%@ Import Namespace = "System.Drawing" %>

<Script runat="server">

private void Page_Load(object sender, System.EventArgs.e)
{

  // Initialize objects,
  string strFilename;
  int newWidth = 64;
  int newHeight = 64;

  // Set the filename.
  strFilename = Server.MapPath("./doggy.jpg");

  // Create a new image from file.
  Bitmap b = new Bitmap(strFilename);
  Bitmap b2 = new Bitmap(b, newWidth, newHeight);

  // Set the content type.
  Response.ContentType="image/jpeg";

  // Send the image to the viewer.
  b2.Save(Response.OutputStream, b.RawFormat);

  // Clean up.
  b.Dispose();

}

</Script>

The result of running this code is that the picture of our little doggie has now been shrunk to a thumbnail:

Click To expand

This script works, but at the moment it has a very rudimentary user interface. Let's build a Web Forms wrapper around the script that will allow the user to select an image from a list and display it on the screen. The first job is to change the code to accept a filename as a parameter:

<%@ Page language="c#" %>

<%@ Import Namespace = "System.Drawing" %>

<Script runat="server">

private void Page_Load(object sender, System.EventArgs e)
{

  // Initialize objects.
  string strSelectedFile;
  string strFilename;
  int newWidth = 64;
  int newHeight = 64;

  // Set the filename.
  strSelectedFi1e = Request.QueryString.Get("filename");

  if(strSelectedFile != null)
  {
    strFilename = Server.MapPath(strSelectedFile);
  }
  else
  {
    strFilename = Server.MapPath("./doggy.jpg");
  }

  // Create a new image from file.
  Bitmap b = new Bitmap(strFilename);
  Bitmap b2 = new Bitmap(b, newWidth, newHeight);
  // Set the content type.
  Response.ContentType="image/jpeg";

  // Send the image to the viewer.
  b2.Save(Response.OutputStream, b.RawFormat);

  // Clean up.
  b.Dispose();

}

</Script>

Next, we need to build the web form user interface, so add a new Web Form to the project and accept the default name of WebForm2.aspx. On the Design surface, place an Image, a Label, and a ListBox taken from the Web Forms tab in the Toolbox.

The ListBox allows us to add some list items by using a special ListItem Collection Editor, which you can launch by clicking the…button next to the Items property in the Properties box. In order to keep things simple, I have just used the filenames of images that I know are in my current folder:

Click To expand

Set the Image control's Visible property to False, as this will keep the image from showing a broken image on the first view of the page. The ListBox requires the AutoPostback property to be True, if we want to be able to view the new image when the list selection is changed, without requiring a Submit button. Finally, blank the Text property of the Label so that no text appears until we update the control when a selection is made.

Your Web Form should look something like this:

Click To expand

If you double-click the ListBox, you will be taken to the code-behind file and insert the following code into the SelectedIndexChanged event handler:

private void ListBox1_SelectedIndexChanged(
  object sender, System.EventArgs e)
{
  if (IsPostBack){ Image1.Visible = true; }
  Image1.ImageUrl = "WebForm1.aspx?filename=" +
    ListBox1.SelectedItem.Value;
  Label1.Text = ListBox1.SelectedItem.Text;
}

Now, if you right-click on WebForm2.aspx, set it as the Start Page, build the project and view it, you should see something like the following:

Click To expand

When the ListBox selection changes, it fires our SelectedIndexChanged event. When this event is called, we set the image to be Visible, and the ImageURL to a string made up of the location of our original .aspx script and the Value of the currently selected ListBox item. Finally we set the Label's Text property to display the caption.


Creating a New Image

So far in this chapter, all of the examples have concentrated on loading and resizing photographic images, which is just one type of application for which this technology is useful. In this section, and for the remainder of the chapter, we will look at how we can create new bitmap images purely from C# code, how to rotate and flip an image, and also how we can create a pie chart.

First, let's take a look at how we create a new image in the first place. Create a new ASP.NET Web Application and in the default Web Form, place this code:

<%@ Page language="c#" %>
<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="System.Drawing.Drawing2D" %>
<%@ Import Namespace="System.Drawing.Text" %>
<Script runat="server">

private void Page_Load(object sender, System.EventArgs e)
{

  // Initialize variables.

  int width = 200;
  int height = 200;

  // Create a new bitmap image.
  Bitmap b = new Bitmap(width, height);
  Graphics g = Graphics.FromImage(b);

  // Blank the bitmap,
  g.Clear(Color.White);

  // Make smooth output.
  g.SmoothingMode = SmoothingMode.AntiAlias;
  g.TextRenderingHint = TextRenderingHint.AntiAlias;

  // Draw some text.
  g.DrawString("Wrox Rox!", new Font("arial",18,FontStyle.Bold), Brushes.DarkBlue,
new PointF(10,50));

  // Serve the bitmap to the browser.
  Response.ContentType = "image/Gif";
  b.Save(Response.OutputStream, ImageFormat.Gif);

  // Clean up.
  b.Dispose();
  g.Dispose();

}

</Script>

This script uses a Bitmap plus a Graphics object to write a string of text to a new GIF image and display it in the user's browser. The Bitmap is created using just Width and Height parameters. We then create a Graphics object and associate it with our Bitmap object using the FromImage method. This Graphics object then performs all of the drawing operations on the Bitmap. We then blank the image white (if we do not do this the default background is black), and set SmoothingMode and TextRenderHint to AntiAlias so that our text comes out looking smooth rather than blocky. Once the bitmap is prepared, we then use the DrawString() method of the Graphics class to output our text with Font, Brush, and Point (left and top X/Y coordinates) parameters.

As all this is happening in shared server memory, it is wise to be careful how much real-time image processing you want to perform on a busy web site, as you may quickly end up with a slow web site because you run out of resources.

When executed, this script should produce something like the following:

Click To expand

Rotating, Flipping, and TextureBrushes

Let's take this example and have some fun. Firstly, we can look at how our new image can include portions drawn using existing images by using the DrawImage() method. Take a look at the following modification to our code.

    .   .   .
    .   .   .
    .   .   .
// Make smooth output.
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = TextRenderingHint.AntiAlias;

// Draw doggy.
Bitmap inB = new Bitmap(Server.MapPath("./doggy.jpg"));
g.DrawImage(inB, 0, 0, 50, 50);
  // Draw some text.
  g.DrawString("Wrox Rox!", new Font("arial",18,FontStyle.Bold), Brushes.DarkBlue,
new PointF(10,50));
      .   .   .
      .   .   .
      .   .   .

This new code sets up a new Bitmap object called inB, based on the existing doggy.jpg image in our local folder (as before, the file system path is acquired using Server.MapPath to translate our web path into the full Windows path). Next, using the Graphic object's DrawImage() method, we draw a 50 pixel by 50 pixel copy of this doggy bitmap to the top-left corner of our new picture:

Click To expand

This is still quite plain, so let's add some rotated and flipped doggies! The modified code is shown highlighted below:

    .   .   .
    .   .   .
    .   .   .
// Make smooth output.
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = TextRenderingHint.AntiAlias;

// Load doggy.
Bitmap inB = new Bitmap(Server.MapPath("./doggy.jpg"));

// Draw doggy rotated and flipped.
inB.RotateFlip(RotateFlipType.Rotate90FlipXY);
g.DrawImage(inB, 0, 0, 50, 50);
inB.RotateFlip(RotateFlipType.Rotate90FlipNone);
g.DrawImage(inB, 51, 0, 50, 50);
inB.RotateFlip(RotateFlipType.Rotates90FlipNone);
g.DrawImage(inB, 102, 0, 50, 50);
  // Draw some text.
  g.DrawString("Wrox Rox!", new Font("arial",18,FontStyle.Bold), Brushes.DarkBlue,
new PointF(10,50));
      .   .   .
      .   .   .
      .   .   .

Here is the result of the code:

Click To expand

As before, we are using DrawImage, but this time we rotate and/or flip the source image before drawing. This is done using the RotateFlip() method of the source Bitmap, which takes a System.Drawing.RotateFlipType as a parameter. There are several of these RotateFlipTypes available covering most of the rotation and flip combinations you would require, allowing you to rotate in increments of 90 degrees and, at the same time, optionally flip on the X and Y axes.

Now we have our tumbling doggies along the top of the image, our text looks quite dull. Let's liven it up by giving it a textured fill and a drop shadow. Edit the code with the modifications shown below:

    .   .   .
    .   .   .
    .   .   .
// Draw doggy rotated and flipped.
inB.RotateFlip(RotateFlipType.Rotate90FlipXY);
g.DrawImage(inB, 0, 0, 50, 50);
inB.RotateFlip(RotateFlipType.Rotate90FlipNone);
g.DrawImage(inB, 51, 0, 50, 50);
inB.RotateFlip(RotateFlipType.Rotate90FlipNone);
g.DrawImage(inB, 102, 0, 50, 50);

// Create a texture brush.
TextureBrush textureBrush = new TextureBrush(inB);
// Draw some text as a shadow.
g.DrawString("Wrox Rox!", new Font("arial",18,FontStyle.Bold),
  Brushes.DarkBlue, new PointF(12,52));

// Draw textured text over top.
g.DrawString("Wrox RoxI", new Font("arial",18,FontStyle.Bold),
  textureBrush, new PointF(10,50));

// Serve the bitmap to the browser.
Response.ContentType = "image/Gif";
b.Save(Response.Outputstream, ImageFormat.Gif);
    .   .   .
    .   .   .
    .   .   .

Below is the result of these modifications:

Click To expand

Here, using our doggy input bitmap as its source, we have created a textured text with a drop shadow by first drawing some text in black, then above and to the left slightly, drawing the same text but with a new System.Drawing.TextureBrush. TextureBrush works much the same as the plain color brush we used in previous examples but allows us to supply a picture to draw the text with, giving the bitmap texture effect.

Pie Charts

Anyone who has used the Web for quite a while will have come across a great deal of statistical data, whether from news sites, corporate annual reports, or stock trading applications. The Web is awash with statistics, data, and charts.

Up until now, if you wanted to create this sort of display dynamically, you would have had to resort to either a custom component or another platform-classic ASP using VBScript just could not create bitmaps or graphically rich charts on the fly, as it had no direct access to the Windows API or GDI.

Now, however, with ASP.NET, we have access to methods for drawing lines, boxes, text, and even pie chart segments. There are now no excuses for not being able to create some pretty impressive graphs!

In the following example, we will be creating a simple pie chart with six different colored sections, although on this printed page, the colors will turn out to be different shades of gray! So, add a new Web Form to the solution and replace the HTML code in the file with the following:

<%@ Page language="c#" %>

<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="System.Drawing.Drawing2D" %>
<%@ Import Namespace="System.Drawing.Text" %>

<Script runat="server">

private void Page_Load(object sender, System.EventArgs e)
{

  // Initialize variables.
  float thisAngle = 0;
  float totalAngle = 0;

  int width = 200;
  int height = width;
  int diameter = width−(width/10);
  int top = (height/2) − (diameter/2);
  int left = (width/2) − (diameter/2);

  // Chart data.
  int[] myData = {60,60,60,60,60,60};

  // Set up color array.
  SolidBrush[] colorBrush = {
    new SolidBrush(Color.Red),
    new SolidBrush(Color.Orange),
    new SolidBrush(Color.Yellow),
    new SolidBrush(Color.Green),
    new SolidBrush(Color.Blue),
    new SolidBrush(Color.Purple),
    new SolidBrush(Color.Black),
    new SolidBrush(Color.Silver)
  };

  // Total of values.
  int myDataTotal = 0;
  foreach(int tmpValue in myData)
  {
      myDataTotal += tmpValue;
  }
  // Create a new bitmap image.
  Bitmap b = new Bitmap(width, height);
  Graphics g = Graphics.FromImage(b);
  g.SmoothingMode = SmoothingMode.AntiAlias;

  // Blank the bitmap.
  g.Clear(Color.Silver);

  // Draw the chart pieces.
  for(int i=0; i<my0ata.Length; i++)
  {
    thisAngle = (Convert.ToSingle(myData[i]) / myDataTotal) * 360;

   g.DrawPie(Pens.White,left,top,diameter,diameter,totalAngle,thisAngle);
   g.FillPie(colorBrush[i],left,top,diameter,diameter,totalAngle,thisAngle);

    totalAngle += thisAngle;
  }

  // Serve the pie chart to the browser.
  Response.ContentType = "image/Gif";
  b.Save(Response.OutputStream, ImageFormat.Gif);

  // Clear up.
  b.Dispose();
  g.Dispose();

}

</Script>

When you execute this code, you should see the following pop up in a new browser window:

Click To expand

In the above code, we create a custom GIF as our pie chart is effectively a line drawing and GIF images give the crispest output. Usually, you would use a JPEG in cases where the image is photographic in nature.

We create the pie chart using the Graphics DrawPie() and FillPie() methods; DrawPie() is used to draw the outlines and FillPie() is used to draw the filled shapes. The shapes are drawn with different colors, selected from an array of brushes.


Summary

This chapter showed how the System.Drawing namespaces can be used in your ASP.NET web applications to deliver dynamic, personalized images to your users and web site visitors.

We saw how we can:

  • Read an image from a file

  • Serve an image to a user's web browser

  • Load an image from a remote web server

  • Save images to the server's file system

  • Modify existing images, such as creating thumbnails from them

  • Create an image on the fly

  • Build charts and graphs

While we have seen how flexible the System.Drawing image capabilities are when accessed through C# and ASP.NET, our examples so far have been quite simple. The next chapter will show how we can expand on the examples and applications we have developed so far and encapsulate some of our newly created functionality into an ASP.NET Web User Control.


Chapter 16: Creating Custom ASP.NET Server Controls

Overview

In the previous chapter, we examined the basic techniques that can be used to create and serve dynamic bitmap images from ASP.NET web applications. While this highlighted some useful functionality that our C# ASP.NET pages can exploit, so far we have only just scratched the surface. We will look at the principles of server-side custom control execution, starting with the basics of building an ASP.NET user control then progressing to how our GDI+ functionality can be used with the controls.

One of the most powerful features of ASP.NET is the capability of creating custom controls. These custom controls give us a comparable level of leverage to Windows Forms custom controls-they provide encapsulation, properties, methods, events, inheritance, and so on. They are similar in nature to Windows Forms custom controls, but are implemented quite differently.

This chapter will explain how to build ASP.NET custom controls that use the GDI+ tools. These custom controls show how we might build easily reusable graphical user interface components for use in web projects. As this chapter progresses, we will discuss the similarities between the Windows and Web controls and the example projects will show how to make ASP.NET controls that have clickable regions. We will also build a thumbnail picture control.

First, we will start off with a bit of background information outlining what a custom control is, the types of control we can create, and why they are so important and useful.


Custom Controls

The simplest form of ASP.NET control, and probably therefore the most common a programmer will create, is the user control. User controls can be thought of as an ASP.NET page within a page. They are very similar in construction to how you would develop a full page in ASP.NET, and with some minor alterations a working page could be converted into a control very easily. As well as being very simple to create, as with ASP.NET pages, you do not need to compile user controls before you use them. This makes rapid development and deployment a real snap. An example of a user control might be an area of the page that shows recent news items. Any page that needs to show news can simply plug the user control into the page.

While being quite basic and on a superficial level somewhat similar to an include for those familiar with classic ASP, their simplicity is their main advantage. No special knowledge or compilation is required to build or deploy them. In fact, some programmers find it easier to prototype controls as full ASP.NET pages and then convert their code into a control. This technique is shown in ISBN 1-86100-703-5, also published by Wrox Press.

In order to gain access to the full power and versatility ASP.NET controls offer, more effort and a little more complex logic is required.

Custom controls on the other hand can be thought of as ‘proper' server controls and are how the server controls that come bundled with ASP.NET were built. If you want your control to be used across applications or are going to package up your control and sell it, then a user control will not provide sufficient functionality and you will have to build a custom control. A good example of a custom control is the Calendar control. This control can be used for everything from scheduling applications to allowing the user to select dates in a form.

While more difficult to create and a little more involved to deploy, a custom control, once created, is actually easier for someone else to use and set properties for in the Visual Studio .NET Design view. Custom controls support additional design-time abilities so a control and its properties can be manipulated in Visual Studio .NET just as the Microsoft controls are.

Although the idea behind controls on the Web is the same as for Windows, there are quite a few differences in implementation between them:

  • Windows controls respond to user interaction instantly in real time-If we want Web Forms controls to respond in real time, we must use a client-side scripting language, such as JavaScript. In Windows, we can do lots of drawing, and mouse and keyboard processing, such as responding to certain key presses or tracking mouse movements; this is a lot more involved to do on the Web. Any reasonable processing in Web Forms requires form information to be posted back to the server. We therefore need to be careful about how user interaction is designed and how responsive we try to make the application over a slow modem.

  • Windows controls can be programmed in one language for one platform-Web Forms must generate HTML, CSS, and JavaScript for multiple clients. Developing a Windows control is a lot more straightforward as there are fewer platform-related variables to test for. Web controls might need to take account of different types of client (such as PC, PDA, set-top box) and multiple channels (for example Interactive TV, WAP, and so on). While web controls can change what they generate based on the capabilities of the browser, this is not always an easy task.

  • Windows Forms custom controls execute on the client-Web Forms controls execute on the server. This is an obvious point, but when designing our web applications, we must take account of the fact that web controls often require a round trip for user interactions to be processed by the server-side application logic. Web Forms can, however, execute code on the client using JavaScript, usually for validation or for aesthetic reasons.


Creating an ASP.NET User Control

As user controls are the easiest to quickly get to grips with, we will start with this type of control and the simplest possible working example. User control creation will seem very familiar, as it is almost identical to creating an ASP.NET page. In fact, they are so similar to create, Microsoft was originally going to call user controls pagelets, which would be a good name as user controls are a lot like include files.

On with the example: create a new C# ASP.NET Web Application and call it Control. This will create a new virtual directory and application on the web server with various files in it, plus a new Web Form file, called WebForm1.aspx by default, so we can start working right away. We will come back to this file later on, but for the time being, add a new Web User Control to the solution and call it test.ascx.

The Design surface will be pretty much blank, other than a message explaining the design layout our control is set to, and the HTML view will open up with code somewhat similar to that below (there may be more text than is displayed here):

<%@ Control Language="c#"
            AutoEventWireup="false"
            Codebehind="test.ascx.cs"
            Inherits="Control.test" %>

This code defines the page as a control, inheriting from Control.test, where Control is the web application namespace and test is this control. The Language attribute says we are using C# and the Codebehind attribute says that the logic is in another, separate code-behind file.

To keep things simple, delete all of the line from after c# to the end of the tag so the line now looks like the following, and save the file:

<%@ Control Language="c#" %>

The code we have just saved is all we need for a basic user control. At this stage, we are going to have all our code in one file and so, we do not need to reference another Codebehind file.

Keep the control file open and go over to the Solution Explorer. Remember the Web Forms file that was created by default along with the new project? Right-click on it and select Properties. Rename it from its current filename (in this case WebForm1.aspx) to a new name, such as ControlTest.aspx and double-click the file to open it up in the editor.

If we now go into the Web Forms Design surface and drag the control file (test.ascx) from the Solution Explorer and drop it onto the page, the control should be visible as a gray button:

Click To expand

While we have the file open, click anywhere on the page to give it focus, then change the title property to something more appropriate, such as User Control Test Page. Save the file and then go into HTML view to see the code. On the second line down from the top, you should see the following code:

<%@ Register TagPrefix="uc1" TagName="test" Src="test.ascx" %>

So far, we haven't entered any code or set any properties, but Visual Studio .NET has set up three default properties for us; TagPrefix, TagName, and Src. This line simply registers the control into the page, adding a pointer to the source file of the control (Src) and gives the control a name and a TagPrefix. The TagPrefix and TagName are used in the HTML code of the page further down, between the form tags.

Let's examine the line that adds the control to the page:

<uc1:test id="Test1" runat="server"></uc1:test>

The line begins with the TagPrefix(uc1) followed by the TagName(test), an id(Test1) and the parameter runat, which states that this control should be processed on the server.

If we were to view this application now, the project would compile, but nothing would be displayed in your browser window. We need to give the control something to output. Go back to the control (test.ascx) and enter the following HTML:

<%@ Control Language="c#" %>

<h1>Hello World from the user control</h1>

Now save the file and go to the Solution Explorer, right-click on the ControlTest.aspx file and select Build and Browse to view the page in the web browser. You should see something like the screenshot below:

Click To expand

What we have done here is entered HTML directly into the user control and this change has been automatically reflected in the Web Form page that references the control. If we imagine this user control as being a standard page header or footer, we could have hundreds of pages referencing this control and all of them would have been updated simultaneously with this one change. A lot less risky than ‘search-and-replace'!

While it is useful to have a file that we can use to include static text in all our pages, user controls are capable of a lot more than this. To make our control more flexible, we should really at the very least allow the developer to set the text to be output. We do this by giving the control a property.

User Control Properties

As we discovered in previous chapters when looking at Windows Forms controls, properties allow a control's values to be read and set by code outside of the control, either at design-time or run-time.

We can add a property to our user control by changing the code as follows:

<%@ Control Language="c#" %>

<script Runat="server">

  string _Caption="Default Caption";
  public string Caption
  {
    get{ return_Caption; }
    set{ _Caption = value; }
  }

</script>

<h1><%=Caption%></h1>

After the first line, which we are already familiar with, is a line that tells the compiler to process the following code on the server. Inside this code block we declare our property. The control has only one property, a string called Caption that we use to set an internal string variable called _Caption (the underscore allows nearly identical names to be used without causing a naming conflict). By default, _Caption is set to contain the message Default Caption. This will help remind developers to set the caption to something meaningful.

Our get() method simply returns the current contents of _Caption and set() writes the contents of value to _Caption. The Caption will be outputted along with the HTML formatting found at the bottom of the code using the shorthand <%= which is used to output the contents of a variable to the response that goes to the user's web browser.

Save the file and Build and Browse the ControlTest.aspx file as before. The page should appear looking like the following:

Click To expand

As we have not set the Caption property to a new value, the default message has appeared. Let's set the Caption now to see our user control properly in action.

Open up ControlTest.aspX and select HTML view. Find the line where our control is specified and change the HTML so that it appears as below:

<uc1:test id=Test1 runat="server" Caption="New Caption"></uc1:test>

This has set the Caption property of our user control to the value New Caption. If you Build and Browse the file now, a page like the following should appear:

Click To expand

This example demonstrates how easy it is to add properties to a simple user control. Let's now build it into something a little more useful.

A Custom Button User Control

In this example, we will develop a control that presents a button to the user, which is fully created through our own code. The final result is achieved with three files; one that is the control calls another that draws the actual button image. The third file is the page in which we want the button to appear. This project will demonstrate how we need to separate the GDI+ code that performs the drawing of the custom bitmap used in the interface from the logic that sends the interface HTML to the users web browser.

The control will allow the designer to set properties for a region of the button to be clickable. Clicking on the hotspot will cause the control to run some event code and display a message set by the designer. If the user clicks anywhere else, a default message is displayed telling the user the X and Y coordinates of where they clicked.

First, we will need to create the page that draws the button image. Add a new Web Form item to your project and call it DrawButton.aspx. Delete the contents of the page and enter the following code:

<%@ Page language="c#" %>

<%@ Import Name space="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="System.Drawing.Drawing2D" %>
<@

// Initialize objects.
Bitmap b = new Bitmap(400, 400, PixelFormat.Format24bppRgb);
Pen blackpen = new Pen(Color.Black,1);
Pen whitepen = new Pen(Color.White,1);
Graphics g = Graphics.FromImage(b);
Font buttonFont = new Font("Arial",12,FontStyle.Bold);
Brush buttonBrush = Brushes.DarkBlue;
StringFormat salign = new StringFormat();
salign.Alignment = StringAlignment.Center;

// Get parameters from querystring.
string strCaption = Request["caption"];
int intTop = Convert.ToInt32(Request["top"]);
int intLeft = Convert.ToInt32(Request["left"]);
int intBottom = Convert.ToInt32(Request["bottom"]);
int intRight = Convert.ToInt32(Request["right"]);
int intHeight = intBottom-intTop;
int intWidth = intRight-intLeft;
PointF captionLocation = new PointF(intLeft+(intWidth/2),
  (intTop−(buttonFont.Height/2)) + (intHeight/2));

// Blank the image.
g.Clear(Color.Silver);

// Antialias objects,
g.SmoothingMode = SmoothingMode.AntiAlias;

// Draw button.
g.DrawString(strCaption,buttonFont,buttonBrush,captionLocation,salign)
g.DrawRectangle(blackpen, intLeft, intTop,intWidth,intHeight);
g.DrawLine(whitepen, intLeft+1, intTop+1, intRight−1, intTop+1);
g.DrawLine(whitepen, intLeft+1,intTop+1,intLeft+1,intBottom−1);
g.DrawLine(blackpen, intRight-1,intTop+2,intRight−1,intBottom−1);
g.DrawLine(blackpen, intLeft+2,intBottom−1,intRight−1,intBottom−1);

// Set the content type.
Response.ContentType="image/jpeg";

// Send the image to the user's browser.
b.Save(Response.OutputStream, ImageFormat.Jpeg);

// clean up
b.Dispose();

@>

There is a lot of code there but it looks more complicated than it really is! The page takes URL parameters of the dimensions of the button hot spot plus the button's caption. Using these parameters, the button is drawn using a rectangle and black and white lines to give a Windows-like 3D effect.

Add a new Web User Control to the project and call it button.ascx. Delete the contents of the new file and replace it with the following:

<%@ Control Language="c#" %>

<script Runat="server">

  string _Caption;
  public string Caption
  {
    get{ return_Caption; }
    set{ _Caption = value; }
  }

  string _HotspotCaption;
  public string HotspotCaption
  {
    get{ return _HotspotCaption; }
    set{ _HotspotCaption = value; }
  }

  int _HotspotTop=0;
  public int HotspotTop
  {
    get{ return _HotspotTop; }
    set{ _HotspotTop = value; }
  }

  int _HotspotLeft=0;
  public int HotspotLeft
  {
    get{ return _HotspotLeft; }
    set{ _HotspotLeft = value; }
  }

  int _HotspotBottom=0;
  public int HotspotBottom
  {
    get{ return _HotspotBottom; }
    set{ _HotspotBottom = value; }
  }

  int _HotspotRight = 0;
  public int HotspotRight
  {
    get{ return _HotspotRight; }
    set{ _HotspotRight = value; }
  }

  public void ButtonImage_Click(
    object sender, System.Web.UI.ImageClickEventArgs e)
  {
    if (e.X > _HotspotLeft && e.Y > _HotspotTop && e.X < _HotspotRight
      && e.Y < _HotspotBottom)
  {
      ButtonImageCaption.Text = _HotspotCaption;
  }
    else
    {
      ButtonImageCaption.Text = "You clicked X:" + e.X + "Y:" + e.Y;
    }
  }

</Script>

<%

ButtonImage.ImageUrl = "DrawButton.aspx?" + "left=" + _HotspotLeft
  "&top=" + _HotspotTop + "&bottom=" + _HotspotBottom + "&right=" +
  _HotspotRight + "&caption=" + _Caption;

ButtonImage.ToolTip=_Caption;
ButtonImage.AlternateText=_Caption;

%>

  <asp:ImageButton id=ButtonImage
                   runat="server"
                   OnClick="ButtonImage_Click"
                   ToolTip=""
                   AlternateText=""
                   ImageUrl=""                                             />
  <br />
  <asp:Label id=ButtonImageCaption runat="server" />

This control has quite a few properties but they are all set up in the same way as the previous example. For this project, we have used an ImageButton and a Label. The ImageButton displays the picture, with a reference to the code we just created previously as a URL in the ImageUrl property. We supply the hotspot coordinates and the caption we wish to display to set the appearance of the bitmap on the query string of the DrawButton.aspx. The resulting ImageButton handles the clicks while the Label shows the messages.

In the HTML for the ImageButton, we specify a function to call when the button is clicked. This function (ButtonImage_Click) checks to see if the user has clicked within the hotspot region, checking against the mouse coordinates returned by the event arguments in System.Web.UI.ImageClickEventArgs. The control will either display the special message specified at design-time or the default, You clicked X: Y:.

To test this control we need to create a new Web Form for the control to be inserted into. In the same way as before, add a new Web Form to the project and call it testButton.aspx. Edit the HTML so that the top two lines appear like this:

<%@ Page Language="c#" %>
<%@ Register TagPrefix="uc1" TagName="button" Src="button.ascx" %>

Next, add the following code between the form tags:

<form id="testButton" method="post" runat="server">
<uc1:button id=Button1
            runat="server"
            Caption="Click Here!"
            HotspotLeft="100"
            HotspotTop="100"
            HotspotRight="200"
            HotspotBottom="160"
            HotspotCaption="You clicked the button!" />
</form>

This code sets the properties for the user control, specifying the hotspot coordinates and captions. Save the page and choose to Build and Browse, or press Ctrl-F8. You should initially see something like the following:

Click To expand

When you click an area of the control other than the hotspot, you will notice that the coordinates of the point clicked are displayed. If you manage to click the hotspot, you will see a screen that appears something like the following screenshot:

Click To expand


Creating a Thumbnail Control

This user control will enable a developer to drop a thumbnail image into a page simply by adding the user control into their HTML and setting one or two properties. The control actually exists in two parts; the . aspx script that serves the thumbnail image and the control itself that requests the thumbnail. We will look at the thumbnail code first.

The need to separate the two pieces is another example of how we need to think differently when moving from Windows to web development. In Windows, we do not have to necessarily separate the code that draws an image from the rest of the control code, whereas in ASP.NET, we are sending both bitmap image data and HTML information to a user's web browser rather than writing directly to the user's screen and there is no way to send both HTML and binary data in the same response to a standard web browser.

Making a Thumbnail

In previous examples where we have resized an image, we have not really taken into consideration the aspect ratio of the image, so the images displayed can appear distorted. This code will display an image resized so it will fit into a ‘box' that the developer specifies, while keeping the original image's aspect ratio.

Create a fresh ASP.NET Web Application and call it Thumbnail. Rename the page created by default to Thumbnail.aspx. Delete HTML any code already present in the .aspx file and replace it with the code below:

We are not using a Codebehind file in this example.

<%@ Page Language="c#" %>

<%@ Import Namespace = "System.Drawing" %>

<%

  // Initialize variables.
  int boxWidth = 100;
  int boxHeight = 100;
  double scale=0;

  // Set the filename.
  string strFilename;
  string strSelectedFile="";

  if(Request["filename"] != null)
  {
    strSelectedFile = Request.QueryString["filename"];
  }
  else
  {
    if(strSelectedFile == "") strSelectedFile="./sample.jpg"
  }
  strFilename = Server.MapPath(strSelectedFile);

  int tmpW = Convert.ToInt.32(Request.QueryString["width"]);
  int tmpH = Convert.ToInt32(Request.QueryString["height"]);

  if(tmpW > 0 && tmpH > 0)
  {
    boxWidth = tmpW;
    boxHeight = tmpH;
  }

  // Create a new image from file.
  Bitmap b = new Bitmap(strFilename);

  // Work out how much to scale.
  if(b.Height < b.width)
  {
    scale = ((double)boxHeight)/b.Width;
  }
  else
  {
    scale = ((double)boxWidth)/b.Height;
  }

  // Set new width and height.
  int newWidth = (int)(scale*b.Width);
  int newHeight = (int)(scale*b.Height);

  // Create output bitmap.
  Bitmap bOut = new Bitmap(b, newWidth, newHeight)
  bOut.Save(Response.OutputStream, b.RawFormat);
  // Clear up.
  b.Dispose();
  bOut.Dispose();

%>

Before you try to run this code you will need to add a picture to your web application to test it with. In the code, we have specified that the name of our image is sample.jpg. Add a new existing item to your project using File | Add Existing Item option from the menu. You will need to change the Files of Type dropdown to allow you to see image files. Navigate until you find a JPEG image on your hard disk and double-click to add it to your project.

When the file appears in the Solution Explorer we can rename it to sample.jpg. You could of course avoid this step by changing the filename referenced in the code, as long as you remember to change it in the following examples as well.

Let's examine the code.

The page takes three Request parameters; one that specifies the image file to load plus the required maximum width and height of the output image. We need the size values to be specified as integers, so we use Convert.ToInt32() to convert from strings to integers. If any of the values are not specified we use defaults.

Next, we load the requested bitmap image file into a new Bitmap object. Our code will create a new one with the correct dimensions from the source image.

Using the source image's Height and Width properties, we determine if the image is landscape or portrait and work out the value that the code will use to scale the new image. All that's left to do then is to create a new image with the dimensions just calculated, and serve this image to the user.

Run the file in the browser to see it working by using Build and Browse as before. A thumbnail to fit a 100 pixel wide box should appear created from the sample image we copied into the application folder.

The two screenshots show the difference in the two images side by side. On the left, we can see that the image has been resized by the control. On the right it the original size screenshot:

Click To expand

Our next task is to create the control that will talk to the thumbnail script, allowing us to let someone effortlessly plop a thumbnail onto their page without worrying about how it will be implemented. This time, we will create a custom control.

Creating a Custom Control

In this example, we are going to create a custom control rather than a user control. Where we have been using .ascx for our user controls, custom controls are created with .cs files, which is the extension Visual Studio .NET uses for a C# class library.

Add a new Class to the project and call it ThumbnailControl.cs. A file similar to the following will be created by Visual Studio .NET:

using System;

namespace Thumbnail
{
  /// <summary>
  /// Summary description for ThumbnailControl.
  /// </summary>
  public class ThumbnailControl
  {
    public ThumbnailControl()
    {
      //
      // TODO: Add constructor logic here
      //
    }
  }
}

We need to modify the code as follows:

using System;

namespace Thumbnail
{
  /// <summary>
  /// Summary description for ThumbnailControl.
  /// </summary>
  public class ThumbnailControl : System.Web.UI.Control
  {
    public ThumbnailControl()
    {
      //
      // TODO: Add constructor logic here
      //
    }

    protected override void Render(System.Web.UI.HtmlTextWriter htOutput)
    {
      htOutput.Write("Test");
    }
  }
}

This class file we have just created is our custom control. All it does at the moment is output the string Test to the page by overriding the default Render event. To see it working, we need to compile it using Build Thumbnail in the Build menu and create a test page.

Add a new Web Form to the project and call it ThumbTest.aspx. Switch to HTML view and modify the first line of the code to keep it simple and add the line following it:

<%@ Page language="c#" %>
<%@ Register TagPrefix="Thumbnail"
             Namespace="Thumbnail" Assembly="Thumbnail" %>

You will notice that this syntax is slightly different from the previous example. This is because we are using a compiled assembly. The compiler will discover all the information it needs by looking at the assembly.

Further down the page, between the form tags, enter the following:

<form id="ThumbTest" method="post" runat="server">
  <Thumbnail:ThumbnailControl runat="server" id=ThumbnailControl1 />
</form>

This will add the control to the page. If you looked at the previous example in Design view, all you would have seen would have been just a gray button. Since this control is a true server control, we actually get to see a visual representation of the control in action:

Click To expand

At the moment, it only outputs some text and has no properties to set. We will rectify both situations together using the following code. Open up ThumbnailControl.cs and insert the following property code:

public class ThumbnailControl : System.Web.UI.Control
{
  string _Src="sample.jpg";
  public string Src
  {
   get{ return _Src; }
   set{ _Src = value; }
 }
.   .
.   .
.   .

Then, replace the Render() method with the one below. This overrides the default Render event and allows us to output a string of HTML when our control is rendered in the user's web browser:

protected override void Render(System.Web.UI.HtmlTextWriter htOutput)
{
  htOutput.Write("<img src=\"thumbnail.aspx?filename= + this.Src + "\">");
}

Our new code, instead of outputting a simple string of text, will now display an image. If you save the file, build it with Ctrl+Shift+B, and then switch to ThumbTest.aspx, you will see that the design view has changed quite significantly to now display our thumbnail image.

To show how easy it would be for a designer or another developer to change the property, add a new image to the project and, staying in design view, go to the Properties window and set the Src property of the ThumbnailControl to other image.


Creating a Thumbnail Picture Gallery

This final exercise displays a page of thumbnails using the pictures in the current folder. It uses a server control supplied with Visual Studio .NET called a Repeater. The Repeater control is used for looping through and displaying lists. Here, it reads and displays each item in a HashTable that is built up using the System.IO.Directory.GetFiles() method.

Add a new Web Form to your project and call it ThumbBrowser.aspx. Clear out any existing code and replace it with the following:

<%@ Register TagPrefix="Thumbnail" Namespace="Thumbnail" Assembly="Thumbnail" %>
<%@ Page language="c#" %>

<html>
  <head>
    <title>Image Browser</title>

    <script runat=server>

      private void Page_Init(object sender, System.EventArgs e)
      {
        //Create a HashTable of values to bind to.
        Hashtable pictures = new Hashtable();

        System.IO.FileInfo file;
        string[] files = null;
        string filename = "";
        string filesize = "";
        string filedate = ""';

        files = System.IO.Directory.GetFiles(Server.MapPath("."),"*.*");

        for(int i= 0; i< files.Length; i++)
        {
          // Create a new FileInfo object for this filename.
          file = new System.IO.FileInfo(files[i]);

          filename = file.Name.ToLower();
          filedate = file.LastWriteTime.ToString("D");
          filesize = file.Length.ToString("n0");

          if(filename.EndsWith(".jpg")||filename.EndsWith(".gif"))
            (pictures[file.Name] = "Name: " + file.Name + "&nbsp;<br />"
              "Date: " + filedate + "&nbsp;<br />" + "Size: " + filesize
              " bytes&nbsp;<br />";}
        }

        repeater1.DataSource = pictures;
        repeater1.DataBind();

      }

    </script>
  </head>
  <body >

    <h1>Image Browser</h1>

    <form method="post" runat="server" ID="Form1">

      <ASP:DataList
        id="repeater1"
        runat="server"
        RepeatLayout="Table"
        RepeatColumns="4"
      >

      <ItemTemplate>

        <!-- link -->
        <a
          href="<%# ((DictionaryEntry)Container.DataItem).Key %>"
          target="_blank"
        >

        <!-- thumbnail pic -->
        <THUMBNAIL:THUMBNAILCONTROL
          id=ThumbnailControl1
          runat="server"
          Src="<%# ((DictionaryEntry)Container.DataItem).Key %>"
        />

        </a><br />

        <!-- file info -->
        <%# ((DictionaryEntry)Container.DataItem).Value %>

      </ItemTemplate>
      </ASP:DataList>

    </form>
  </body>
</html>

Providing there are images in your current folder, a page similar to the following will appear:

Click To expand


Design-Time Support

One of the advantages of custom controls over user controls is the ability to add features to make the control friendlier to use in Visual Studio .NET. First of all, we can create an icon image for the control and add the control to the Toolbox.

Creating a Toolbox Icon

Go to the File menu and select Add New Item. When the dialog appears, scroll down and select to create a new Bitmap File, calling it ThumbnailControl.bmp so it has the same name as our custom control.

The picture must be a 16-color bitmap, 16 pixels high and 16 pixels wide. Draw a design for your icon and save it. Then, from the Solution Explorer, highlight the file and in the Properties window, set the Build Action to Embedded Resource as in the screenshot:

Click To expand

Go to the Build menu and select Rebuild Thumbnail. Now we can add our control to the Toolbox and the new icon we created should appear, so bring up the Design window, right-click on the Toolbox and select Customize Toolbox. Choose the .NET Framework Components tab and click the Browse… button. Navigate the folders to the location of the project's web folder. Here, there will be a bin directory containing the control's .dll file. After you select the file and click on OK, the control should appear, with its new icon, in the toolbox, just like the Microsoft controls.

Click To expand

Other Design-Time Features

When we build custom controls for Web Forms, we can modify their design-time characteristics in a similar fashion to how we configure the design-time characteristics of Windows Forms controls.

At the top of the .cs file we need to add the following line to allow us to use these attributes.

using System.ComponentModel;

The following code sample shows how you can add a description of a property to the Properties box, as well as telling the Properties box which category the property should appear under:

string _Src="sample.jpg";
[
Category("Appearance"),
Description("Source Image")
]
public string Src
{
  get{ return _Src; }
  set{ _Src = value; }
}

The following screenshot shows how this affects the Properties window:

Click To expand

More details about design-time features can be found in .


Summary

In this chapter we looked at how we can build custom controls in Web Forms. We examined the different types of server controls that we can build and what the differences between them are, then we went on to build a couple of user controls and a custom control, ending with a thumbnail picture browser that uses the new control.

While this chapter has only really managed to give a quick overview, there should be enough here to give you a good start to building Web Form controls. For more details on implementing custom controls with C#, refer to Professional ASP.NET Server Controls: Building Custom Controls with C# from Wrox Press, ISBN 1-86100-564-4.

In the next chapter we look further at creating reusable code, but this time, the code will be executed on remote servers using Web Services.


Chapter 17: Web Services and GDI+

Overview

The last two chapters have explained how to use GDI+ features in web pages, first creating web pages that use GDI+ code directly, and then wrapped up in user and custom server controls. In this, the third and last web-based chapter, we look at how GDI+ can be used when developing web services that return images.

Web services have received a lot of media attention lately, with a lot of commentators declaring the technology to be the Next Big Thing. Microsoft in particular has championed web services and invested heavily in developer tools, with other big name companies such as IBM and Sun desperately pushing their own implementations and services to maintain their market and mind share.

This chapter will first explain what web services are, and then we will develop three different example web services. We will begin with the simplest web service and increase complexity through to an example web service that returns a custom image. First, we will look at what web services are, why they are seen to be so important, and how and where GDI+ fits in.


What are Web Services?

Web services are a means by which logic can be executed over standard web protocols. Remote servers or the machine your code lives on could process this logic, and the web service can be written in a different language on a different platform from the consuming program. For example, a Windows desktop application written in Visual Basic .NET running on a PC in London could consume a web service written in Java, hosted on a Linux server in Washington.

Many people see web services as the future of the Internet, with web services running in the background providing data and functionality for everything from the fridge to your wristwatch! While web services might not infiltrate every area of computing, you can bet they are going to be an important addition to your programming repertoire.

Web services are not as revolutionary as this suggests. Executing code on different computers is not new; remote procedure calls through DCOM and CORBA have formed the foundation of many enterprise applications for quite a long time. The main advantage web services have over these other technologies is the fact that web services are based on the XML and HTTP standards, meaning no tricky firewall configurations are required and the minimum specification of machine to use this technology is not very demanding. In .NET, web service calls are simple text messages sent via simple HTTP GET or POST.

Even performing remote procedure calls over web protocols is not particularly a new idea as a software company called Userland was promoting XML-RPC, an alternative to SOAP, for a few years before the rest of the industry jumped on the web service bandwagon. Check out http://www.scripting.com for more details.

Part of the reason web services are becoming such a hot technology is the implementation that Microsoft has developed for the .NET Framework. As well as the full standards shopping list of acronyms being supported, Microsoft has made creating and consuming web services incredibly easy in .NET, and even easier with Visual Studio .NET.

As well as XML and HTTP, the other important standards that web services rely on are SOAP, WSDL, and UDDI:

  • SOAP stands for Simple Object Access Protocol, and it defines the format of the XML used in the procedure call and the web service's response.

  • Web services' implementations are described by WSDL, the Web Services Description Language. Using the WSDL information of a web service, developers and tools can determine what methods are exposed by the service and how to use them.

  • UDDI, Universal Description, Discovery, and Integration, is the standard upon which directories of web services can be built, allowing developers to ‘shop' for web services like you would find a web site with Yahoo.


How can GDI+ be used with a Web Service?

There are three main ways a web service can be used when developing a web application that relies on custom images and we will examine these different approaches in this chapter.

The first is the simplest, where the web service returns data that is then used to create an image. An example of this could be a web application that needs to display an image representing the current time for Nice in the south of France. The web application might create a bitmap based on a time supplied by a web service, incorporating sun and moon icons or an analog clock face.

Where the web service creates or supplies an image, there are two options; the web service can either return the image data or a URL for the image.

The URL approach is used by the thumbnail service example at Microsoft's http://www.GotDotNet.com web site. This web service will create a screenshot thumbnail of any web site whose URL you specify. The URL returned can be used to display the resulting image for a short time before the image file is deleted.

Web services that return actual image file data could include, for example, a service that allows you to specify car journey details and have returned a route map as a GIF image, or a more advanced web service might be developed that allows programmers to supply a bitmap image and have custom filters applied of the type you can buy as plug-ins for Adobe Photoshop.


A Simple Web Service

To kick off, we will develop a very simple web service that simply returns the string Hello World. This will allow us to familiarize ourselves with creating web services with Visual Studio .NET and how we can consume the web services we create in our applications.

The Hello World Web Service

In Visual Studio .NET, create a new ASP.NET Web Service and call it HelloWorldService. Rename the Service1.asmx file to HelloWorldService.asmx.

For these examples, we will be running both the web service and the consuming application on the same machine but there would be very little difference if you were developing the web service and consuming applications on different machines.

In the main window, you will see a message explaining that you can drag components to the class, followed by a link:

Note

To add components to your class, drag them from the Server Explorer or Toolbox and use the Properties window to set their properties. To create methods and events for your class, click here to switch to code view.

Over in the Properties window, change the (Name) property of the service from Service1 to HelloWorldService.

Next, click the link that takes you to the code view. The code window will display the Visual Studio .NET generated code. If you look at the code, you will see that our web service has been built for us; it already has a method that when called returns Hello World! The method has been commented out though.

We do not want to write any new code as yet, we are simply learning how to create and consume a web service with Visual Studio .NET. At this stage, all we need to do is uncomment the lines between [WebMethod] and the method's closing brace, as in the following code snippet:

[WebMethod]
public string HelloWorld()
{
  return "Hello World";
}

A WebMethod is the attribute we use to tell the compiler that this piece of code can be executed remotely. You can think of it as a method of a class, but one that is executed over the Web.

After this is done and the files changes have been saved, hit Ctrl-F5 to test our new service. A web page will appear that looks like the screenshot below:

Click To expand

At the top of the page will be two links. The first link takes you to the service's Service Description and the second link is the web method we just created. Click the first link and you will be taken to the web service's WSDL description.

Luckily, it is not absolutely necessary when starting out, to understand the WSDL description, or be able to write it from scratch, as it is created for us automatically. If you go back to the original page and click the second link, you will be taken to a screen like the one below, which displays a button that will allow us to invoke the WebMethod:

Click To expand

This screen shows example SOAP messages, which, again, we do not necessarily need to be able to read or write, and an Invoke button that allows us to test to see if our web method works. Hitting the Invoke button yields this screen, showing the XML-formatted output of the web service's WebMethod, and our string, Hello World:

Click To expand

So far, we have created a web service and tested it using a web browser, but this is only the server half of a web service operation, we now need to write a test application that will consume our new web service. To illustrate consuming a service, we will create a simple Windows application.

Add a new Windows Application to the project, call it TestHello and make it the startup project. Right-click on the TestHello project and click on Add Web Reference…to enter the address of the web service. As we are consuming a .NET web service, we just need to append the address with ?WSDL, for example, http://localhost/HelloWorldService/HelloWorldService.asmx?WSDL. Then, click on the Goto button and click the Add Reference button.

Place a Button from the toolbox to the form and double-click on it. In the code window that appears, modify the button's Click event:

private void button1_Click(object sender, System.EventArgs e)
{
  localhost.HelloWorldService myHello =
    new localhost.HelloWorldService();
  MessageBox.Show(myHello.HelloWorld());
}

This code creates a new instance object of a proxy class that will connect to our web service for us. If we were using the .NET command line tools, we would need to generate the proxy manually using wsdl.exe-Visual Studio .NET does this for us at build-time. We then show a MessageBox that displays the response from the web service's HelloWorld() method.

When you have saved this file, press Ctrl-F5 to launch our test app. After the program has compiled and launched, a window with a button should appear. When this is clicked, the application displays a dialog containing the words Hello World, the response from our web service:

Click To expand

As you can see, Visual Studio .NET hides all the complexities of creating and consuming web services from us. We did not have to worry about WSDL, SOAP, or creating a proxy class. Just a few clicks, a couple of lines of code and in about ten minutes or less we have created and consumed a brand new web service!

At the start of this chapter, we discussed that a web service can return information, the location of an image, or image data. Our first example was a very basic example of a web service that returns information. It is not at all difficult to imagine a web service that returns a string that is then used as the source text to a DrawString function, perhaps as a translation for a multi-lingual web site or maybe a dictionary ‘word of the day' service. Our next example shows how the second type, a web service that returns the location of an image, can be developed and consumed.


Returning an Image URL

This time, we will develop the client application first and then we will modify it to use a web service.

The application will be for viewing pictures loaded from the Internet. Anyone using a copy of the application anywhere can set the image that all the other users view, selecting a URL using the TextBox and Button located at the top of the form. Clicking another button at the bottom of the form loads in the image from the URL currently set by loading the new image location from the web service. The web service will have two methods, one to set the URL and the other to request the current URL.

Create a new Windows Application and call it ImageView. A form will be created called Form1. Highlight the form, go over to the Properties window and change the Text property to Image Viewer. This caption will appear in the form's title bar when the application is running.

Now we have a form we need to add the controls. The main form element we need is the PictureBox, so we will add this first. Go to the Toolbox and drag a PictureBox to the form, resizing it to fit the majority of the form, but leaving enough space above for a TextBox and a Button, and below for another Button. Set the BorderStyle property to Fixed3D so that the PictureBox is clearly visible at all times.

We now need to add the TextBox that will be used to set an image URL, which needs to be quite long. Drag the TextBox control to the top of the form, sizing it to fit around two-thirds the width of the form. In its Properties window, delete the default text that appears in the Text property, and leave it blank. Set the (Name) property to URL.

Next, we need to draw our two Buttons. Firstly, the top Button that will be used to set the URL. Drag a new Button onto the form and call it SetURLButton using the (Name) property and set the Text property to Set URL. When this is done, drag another Button to the space below the PictureBox. This will be used to load the image into the PictureBox, so we will call it LoadImageButton, and set its Text property to Load Image.

At this point, the form will look like this:

Click To expand

Now we need to add some code to make the application actually do something. So that we can test the application before we develop the web service, we will ignore the SetURL button and the URL textbox, and develop the rest of the code with a hard-coded URL.

Double-click the Load Image button and add the following code to its Click event. You might recognize this code as very similar to the code we created in Chapter 15, which connected to a webcam's web site and pulled down a picture of what was happening in London:

private void LoadingImageButton_Click(object sender, System.EventArgs e)
{
  // Initialize the web object.
  WebClient webC = new WebClient();
  // Set the URL.
  string strFilename;
  strFilename="http://localhost/doggy.jpg";
  // Create a bitmap based on the image from the URL.
  Bitmap b = new Bitmap(webC.OpenRead(strFilename));
  pictureBox1.Image = b;
}

You will need to change the line that starts with strFilename = to an image URL you know is accessible from your development PC.

Before we test our new application, we also need to add a reference to System.Net so that we can create a WebClient object that will pull our image down from the web site specified in the URL. Simply scroll to the top of the code window and add the additional line as below.

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Net;

If we compile and run the application, something similar to the following screenshot will be displayed, with a different image displayed when you click the Load Image button obviously:

Click To expand

We are half way there now. Our application will load an image from a URL we have specified, albeit in hard-coded form. Next, we need to take our single-user application and make it store and retrieve the URL string from a central web service.

The Image URL Web Service

Our web service will take a URL set as a parameter to the SetURL() web method and store it in an application variable. When a client application requests the URL from the GetURL() web method, whatever URL is in the application variable will be returned. To keep the example code simple, we will not attempt to validate the URL.

Add a new ASP.NET Web Service to the solution and call it ImageURLService. Rename the file called Service1.asmx to ImageURLService.asmx. Click the link to take you to code view and scroll down to the commented out HelloWorld() web method. Unlike before where we simply took out the comment slashes, we will replace that method with the two methods shown below:

[WebMethod]
public string GetURL()
{

  if( Application["strImageURL"] ==null ||
    Application["strImageURL"]=="" )
  {
    Application["strImageURL"]= "http://localhost/doggy.jpg";
  }
  return Application["strImageURL"].ToString();
}

[WebMethod]
public string SetURL(string strURL)
{
  if( strURL!=null && strURL!="" )
  {
    Application["strImageURL"]= strURL;
  }
  return Application["strImageURL"].ToString();
}

These two methods will set and get the value of the application variable strImageURL. If this variable is blank, then a default (the same URL as before) is used. Set this project as the startup project, and compile and run the application to launch the web service in your browser. You should see a screen with three links:

  • Service Description

  • SetURL

  • GetURL

We can test the SetURL() method by clicking on its link, entering an appropriate value for strURL, and clicking the Invoke button. The resulting SOAP XML will show that the SetURL() method has been successful. Similarly, we can test the GetURL() method by clicking on its link, pressing the Invoke button and inspecting the SOAP XML.

All we need to do now is complete our client application to connect to our new web service.

Right-click on the ImageView project and click on Add Web Reference…to enter the address of the web service's WSDL service description. In my case, this is http://localhost/lmageURLService/lmageURLService.asmx?WSDL. Then click on the Add Reference button.

Next, double-click the form in the Solution Explorer, and when the form has loaded, double-click the Load Image button to view its Click event. Modify the code to be like the following:

private void LoadingImageButton_Click(object sender, System.EventArgs e)
{
  // Initialize object to connect to web service.
  localhost.ImageURLService wService;
  wService = new localhost.ImageURLService();

  // Initialize the web object.
  WebClient webC = new WebClient();

  // Set the URL.
  string strFilename;
  strFilename=wService.GetURL();
  wService.Dispose();

  // Create a bitmap based on the image from the URL.
  Bitmap b = new Bitmap(webC.OpenRead(strFilename));
  pictureBox1.Image = b;
}

This change to the code makes the application connect to the web service, and using the web service's GetURL() web method, returns the currently set image URL as the location to read the image from, rather than our hard-coded value we had previously.

If you set the ImageView project to be the startup project and launch the application, we will see something like the original screen we saw a few pages ago. As we have not set a value yet, the default has been used, which in this particular case is the URL for a picture of a cute little dog. Now we will add the code to allow us to set the URL of the picture we want everyone to see.

Close down the application if it is running, then go back to the form design view. Double-click the Set URL button and insert the following code into its Click event:

private void SetURLButton_Click(object sender, System.EventArgs e)
{
  localhost.ImageURLService wService;
  wService = new localhost.ImageURLService();
  wService.SetURL(URL.Text);
  wService.Dispose();
}

This piece of code sends whatever has been typed into the URL textbox as the URL string parameter to the SetURL() web method of the web service. If you launch the application now, and enter a valid web URL of an image into the textbox and click the Set URL button, clicking Load Image this time means you should see the image referenced by the new URL you have just set.

Had this web service been deployed to a publicly available server, anyone using a copy of your application would now be able to click Load Image and see exactly the same picture.

In our next example project, we will write a web service that, instead of returning a URL, returns the actual image data for the client application to present or process.


Returning Image Data

For this example, we will modify the code we developed in Chapter 15 that returns an image of a pie chart with the data supplied as four parameters to the web method to represent regional sales figures. As a departure from the other examples, this time we will consume the web service in an ASP.NET page.

We will start by creating the web service, so create a new ASP.NET Web Service and call it PieService. Rename Service1.asmx to PieService.asmx. Also, change the (Name) property of the web service to PieService. Make sure you have a reference to System.Drawing.dll in the project. In the code for PieService.asmx, we will also need to add two using statements at the top of the file, one for System.Drawing and the other for System.Drawing.Imaging:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using System.Drawing;
using System.Drawing.Imaging;

As before, we will modify the WebMethod that Visual Studio .NET sets up for us. Change the method so it appears as below:

[WebMethod]
public Byte[] DrawChart(
  int north,
  int east,
  int south,
  int west)
{

  // Initialize variables.
  System.IO.MemoryStream outStream;
  outStream = new System.IO.MemoryStream();
  float thisAngle = 0;
  float totalAngle = 0;

  int width = 200;
  int height = width;
  int diameter = width-(width/10);
  int top = (height/2)−(diameter/2);
  int left = (width/2)−(diameter/2);

  // Chart data.
  int[] myData = {north, east, south, west};

  // Set up color array.
  SolidBrush[] colorBrush = {
                              new SolidBrush(Color.Red),
                              new SolidBrush(Color.Yellow),
                              new SolidBrush(Color. Blue),
                              new SolidBrush(Color.Cyan)
                            };

  // Total of values.
  int myDataTotal = 0;
  foreach(int tmpValue in myData)
  {
    myDataTotal += tmpValue;
  }

  // Create a new bitmap image.
  Bitmap b = new Bitmap(width, height);
  Graphics g = Graphics.FromImage(b);

  // Blank the bitmap.
  g.Clear(Color.Black);

  // Draw the chart pieces.
  for(int i=0;i<myData.Length; i++)
  {
    thisAngle = (Convert.ToSingle(myData[i]) / myDataTotal) * 360;

    //g.DrawPie(
      Pens.Black,left,top,diameter,diameter,totalAngle,thisAngle);
    g.FillPie(
      colorBrush[i],left,top,diameter,diameter,totalAngle,thisAngle);

    totalAngle += thisAngle;
  }

  // Serve the pie chart to the client.
  b.Save(outStream, ImageFormat.Gif);

  return outStream.ToArray();

  // Clear up.
  b.Dispose();
  g.Dispose();
}

This code is a lot like the routine we developed in Chapter 15, but there are a couple of major differences. The first difference is that this is a web method returning an array of bytes and taking four integer parameters for the four sales regions. The second difference is that rather than output the image to the user directly, it instead builds the pie chart based on the supplied parameters and then returns the image data using a stream.

When launched, this web service should produce output like in the following screenshot below:

Click To expand

This screenshot shows the web service DrawChart method being tested with values and the XML output with the binary data base-64 encoded. It highlights the main limitation of using a web service in this way to return binary data in that the binary must be converted to a format that is transmittable using a textual representation, incurring a larger overhead as the amount of data increases. This is something to keep in mind when selecting an approach for your solution.

Now we will create the client web application, so add an ASP.NET Web Application to the solution and call it TestPieService. Rename the web form that is created, calling it default.aspx. After the file has been renamed, make the TestPieService project the startup project and set default.aspx as the start page by right-clicking on it and selecting the appropriate option from the drop-down list.

In the HTML view, delete all but the very top line-as we will be returning an image, we do not want any HTML at all to be output:

<%@ Page language="c#"
    Codebehind="default.aspx.cs"
    AutoEventWireup="false"
    Inherits="TestPieService.WebForm1"
%>

Now go back to the Design view and double-click the form, which will take us to the page's code-behind file. Modify the Page_Load code to reflect the code shown below:

private void Page_Load(object sender, System.EventArgs e)
{
  // Put user code to initialize the page here
  // Initialize objects.
  localhost.PieService wService;
  wService = new localhost.PieService();
  System.Drawing.Bitmap b;
  Byte[] bytes;
  System.IO.MemoryStream inStream;
  // Get image data.
  bytes = wService.DrawChart(100,60,30,5);
  inStream = new System.IO.MemoryStream(bytes)

  // Create a new image from file.
  b = new System.Drawing.Bitmap(inStream);

  // Set the content type.
  Response.ContentType="image/gif";

  // Send the image to the viewer.
  b.Save(Response.OutputStream, b.RawFormat);

  // Tidy up.
  b.Dispose();
}

This code creates a new bitmap based on data pulled from the web service using a proxy object (wService) and a stream (inStream). After this, it is not much different to the examples from Chapter 15.

The final thing we need to do is add a web reference to the project so this application can see the web service. To do this, right-click the test project in the Solution Explorer and choose Add Web Reference…to supply the web service's WSDL, exactly as we did with the Windows Forms examples earlier. I used http://localhost/PieService/PieService.asmx?WSDL If you now run the application, a web browser window like the one in the following screenshot should appear:

Click To expand


Summary

This chapter set out to give a guided tour of the approaches to developing and consuming web services that build on GDI+ functionality. While there is possibly a fair amount of work that would be required to build these examples into truly useful real-world applications, they do give an indication of how you might address the various concepts involved.

In the chapter, we looked at the ways web services and GDI+ can be used together, whether serving information to be used in the GDI+ application or even creating the image within the service and delivering binary data. We also looked at how both Windows and web applications can consume these services.

Over the coming months, expect to see a great deal of new web services arriving as programmers around the world start to get to grips with the different way of looking at their applications. Already, many developers are looking at their applications' functionality and seeing how they can expose the applications' data using web services and XML, from Google.com with its search API to the Amazon.com associates program that allows programmers to retrieve product information as XML. Not all of these services will necessarily use Visual Studio. NET, .NET, or GDI+, or even SOAP, but you know now that using these tools will give you an edge that will make your web service even more outstanding.

Web services are likely to be a hot topic in programming for quite some time and combining web services with GDI+, we have the tools to create some exciting new applications. As a developer, it will be a useful, valuable and most of all, lucrative tool to develop in your arsenal!


Previous Section Next Section

Index

Symbol

−operator, Point structure, 59
−operator, size structure, 60
!=operator, Point structure, 59
!=operator, Rectangle structure, 61
!=operator, size structure, 60
.NET framework, 33
\t escape sequence, 112-13
+operator, Point structure, 59
+operator, Size structure, 60
==operator, Point structure, 59
==operator, Rectangle structure, 61
==operator, Size structure, 60


Previous Section Next Section

Index

Number

3D effects, 282-86


Previous Section Next Section

Index

A

Add Reference dialog, Visual Studio .NET, 29
AddBezier method, GraphicsPath class, 173
AddLine method, GraphicsPath class, 178
AddString method, GraphicsPath class, 195
affine transformations, 222
aliasing, 54
Alignment property, Pen class, 69
Alignment property, StringFormat class, 315
AllowSelection property, Windows Forms PrintDialog class, 249
AllowSomePages property, Windows Forms PrintDialog class, 252
Alpha components, 42-44
images, 162-65
Anchor property, Windows Forms controls, 359
animation, 165-67
anti-aliasing, 84
ascenders, fonts, 115
ascents, fonts, 119-20
ASP.NET, 446-69
server controls, 471-90
Web Forms, 459
web services, 493-507
Assert method, Debug class, 253
AutoScroll property, Windows Forms Panel class, 386
AutoScrollMinSize property, Windows Forms Panel class, 386-87
AutoScrollPosition property, Windows Forms Panel class, 388


Previous Section Next Section

Index

B

baselines, fonts, 103
BeginPrint event, PrintDocument class, 242
BitBlt function, 421-25
bitmap block transfer (see BitBlt function )
Bitmap class, 483
constructor, 153-54
Height property, 142
HorizontalResolution property, 142
performance, Bitmap objects, 155
RotateFlip method, 465
Save method, 168
scope, Bitmap objects, 155
SetResolution method, 145
VerticalResolution property, 142
width property, 142
bitmap fonts ( see raster fonts )
bitmaps, 135-36
color depths, 135-36
compression, 136
creating a bitmap, 153-56
resolution, 145-46
BMP format, images, 167
bounding rectangles, text, 101-3
Bounds property, PageSettings class, 259
brightness, 42
Brush class, 283
brushes, 113-14
Brushes class, 90
buffering, images, 156-57


Previous Section Next Section

Index

C

C++, 50
Cancel property, CancelEventArgs class, 331
CancelEventArgs class, 331
CanConvertFrom method, TypeConverter class, 352
CanConvertTo method, TypeConverter class, 352
Capture property, Windows Forms Control class, 414
Cartesian coordinates, 38
Ceiling method, Point structure, 60
CenterColor property, PathGradientBrush class, 88
class libraries, 15-16
Clear method, Graphics class, 48
ClearMarkers method, GraphicsPath class, 180
ClearType anti-aliasing, 122-25
Click event, Windows Forms controls, 405
Click event, Windows Forms Form class, 213-14
ClientRectangle property, Windows Forms controls, 297
ClientRectangle property, Windows Forms Form class, 45
ClientSize property, Windows Forms Form class, 45
ClientToScreen function, 428
Clip property, Graphics class, 198
ClipBounds property, Graphics class, 198
clipping, 235
clipping regions, 214-15
clipping retangles, 214
ClipRectangle property, Windows Forms PaintEventArgs class, 212
Clone method, GraphicsPath class, 180
Clone method, Image class, 151
Clone method, Region class, 185
CloseAllFigures method, GraphicsPath class, 180
closed GraphicsPath objects, 174
CloseFigure method, GraphicsPath class, 180
Codebehind attribute, ASP.NET Control directives, 473
code-behind files, 473
codecs, images, 136-37
color depths, 47
bitmaps, 135-36
dithering, 41
forms, 47
images, 47
color gradients, 84
color intensity (see saturation )
Color property, PageSettings class, 265-66
Color property, Windows Forms controls, 359
Color structure, 75
FromArgb method, 430
GetBrightness method, 42
GetHue method, 42
GetSaturation method, 42
ColorMatrix class, 162-64
colors, 41-44
Alpha components, 42-44
brightness, 42
hue, 42
saturation, 42
CombineMode enumeration, 214
Command Prompt window, Visual Studio .NET, 31-32
Complement method, Region class, 184
Component class, 293
components, 290-94
composite controls, Windows Forms, 333-37
composite properties, 345
CompositingMode enumeration, 158 -59
CompositingMode property, Graphics class, 158-60
compression, bitmaps, 136
Console class, 30-31
constituent controls, 294, 391
Contains method, Rectangle structure, 409
ContainsFocus property, Windows Forms controls, 315
ContentType property, HttpResponse class, 452
Control class, Windows Forms, 313
Capture property, 414
Click event, 405
ClientRectangle property, 297
ContainsFocus property, 315
DoubleClick event, 405
GotFocus event, 317
Invalidate method, 317
IsMnemonic method, 319
LostFocus event, 317
MouseDown event, 437
MouseEnter event, 404
MouseHover event, 404
MouseLeave event, 404
MouseMove event, 418, 441
MouseUp event, 439
MouseWheel event, 404
ProcessMnemonic method, 318-19
Validated event, 340
Validating event, 340
Control property, SystemColors class, 283
Control property, Windows Forms ControlDesigner class, 379
ControlDark property, SystemColors class, 283
ControlDarkDark property, SystemColors class, 283
ControlDesigner class, Windows Forms, 378-79
Control property, 379
OnPaintAdornments method, 379
OnSetCursor method, 379
Verbs property, 375
WndProc method, 381
ControlLight property, SystemColors class, 283
ControlLightLight property, SystemColors class, 283
controls, ASP.NET (see custom server controls )
controls, Windows Forms
custom controls, 289-341
user controls, 294
mouse cursors, 405-9
custom mouse cursors, 408-9
mouse events, 409-21
non-scrollable controls, 392-93
ControlStyles enumeration, Windows Forms, 157
ConvertFrom method, TypeConverter class, 352
ConvertTo method, TypeConverter class, 352
coordinate systems, 271-86
3D effects, 282-86
anti-aliasing, 84
device coordinate system, 235
floating-point coordinates, 61-62
origin, 62
Outline Model coordinate system, 272
page coordinate system, 221
units of measurement, 57-58
world coordinate system, 221
CreateDC function, 424
CreateGraphics method, Windows Forms Form class, 48
CreateInstance method, TypeConverter class, 355-56
Current property, Windows Forms Cursor class, 407
Cursor class, Windows Forms, 405
Current property, 407
cursors (see mouse cursors )
Cursors class, Windows Forms, 406
Default property, 406
WaitCursor property, 408
custom controls (see also 17-20
Web Forms, 18-20
Windows Forms, 289-341
composite controls, 333-37
events, 340
focus, 310-22
non-procedural constructs, 338-39
properties, 306-10
user controls, 294
custom designers, 373-82
custom mouse cursors, 408-9
custom server controls (see server controls )
CustomControlDefaultPropValues class (example), 306-8
CustomEndCap property, Pen class, 74
CustomLineCap class, 74
CustomStartCap property, Pen class, 74


Previous Section Next Section

Index

D

dashed lines, 69-72
DashPattern property, Pen class, 71
DashStyle enumeration, 69-70
DashStyle property, Pen class, 71
Debug class, 253
debugging, 358-59
Decoder class, 137
decoders, images, 136-37
decorator patterns, 281
default fonts, 101
Default property, Windows Forms Cursors class, 406
DefaultPageSettings property, PrintDocument class, 266
delegates, 322-23
descenders, fonts, 115
descents, fonts, 118, 119-20
design patterns, 281
design units, fonts, 118
Design view, Visual Studio, 346-47
DesignerVerb class, Windows Forms, 375
DesignerVerbCollection class, Windows Forms, 375
device contexts, 421
device coordinate system, 235
DirectX, 16
Dispose method, IDisposable interface, 33-34
Dispose method, Pen class, 75
distributability, components, 293
dithering, 41
DllImport attribute, 424, 426-27
Document property, Windows Forms PrintPreviewDialog class, 243
documentation, IIS, 448-49
double-buffering, images, 156-57
DoubleClick event, Windows Forms controls, 405
DpiX property, Graphics class, 260
DpiY property, Graphics class, 260
dragging, 425-32
drag-scrolling, 432-33
DrawArc method, Graphics class, 48
DrawBezier method, Graphics class, 49
DrawCurve method, Graphics class, 49
DrawElipse method, Graphics class, 49
DrawIcon method, Graphics class, 49
DrawImage method, Graphics class, 421, 463-65
drawing surfaces, 37-63
Cartesian coordinates, 38
color depths, 47
dithering, 41
images, 46
pixel resolutions, 39
pixels, 38
printers, 45-46
raster-based drawing surfaces, 38
resolution, 47
size, 47
vector-based drawing surfaces, 38
Windows Forms, 44-45
DrawLine method, Graphics class, 119-20
DrawPath method, Graphics class, 49
DrawPie method, Graphics class, 469
DrawPolygon method, Graphics class, 49
DrawRectangle method, Graphics class, 280
DrawString method, Graphics class, 463
DropDownControl method, IWindowsFormsEditorService interface, 372
dynamic images, 455-57


Previous Section Next Section

Index

E

Editvalue method, UITypeEditor class, 371-72
em heights, fonts, 116
em sizes, fonts , 117-18
em squares, fonts, 116
em units, fonts, 115-16
encapsulation, components, 291
Encoder class, 168
EncoderParameter class, 168
EncoderParameters class, 168
encoders, Images, 136-37
EndCap property, Pen class, 72-73
EndPrint event, PrintDocument class, 242
ErrorProvider class, Windows Forms, 328
SetError method, 332
event keyword, 323
events, 347
components, 292
mouse events, 409-21
Windows Forms, 340
Exclude method, Region class, 184
ExcludeClip method, Graphics class, 197
EXIF format, images, 135


Previous Section Next Section

Index

F

FillClosedCurve method, Graphics class, 49
FillEllipse method, Graphics class, 49
FillMode property, GraphicsPath class, 179
FillPath method, Graphics class, 177
FillPie method, Graphics class, 469
FillPolygon method, Graphics class, 50
FillRectangle method, Graphics class, 297
FillRegion method, Graphics class, 50
Flatten method, GraphicsPath class, 180
floating-point coordinates, 61-62
focus cues, 310-11
focus, Windows Forms custom controls, 310-22
Font class, 99-101
constructor, 99-100
Dispose method, 100
Height property, 102
Size property, 117
font families, 97
metrics, 116-17
font heights, 115
font styles, 104-5
FontFamily class, 118-20
constructor, 98
GetFamilies method, 114
fonts, 114-27
ClearType anti-aliasing, 122-25
default fonts, 101
metrics, 115-21
quality, 121-27
raster fonts, 97
resolution, 116-21
TrueType fonts, 97
Fontstyle enumeration, 105
Form class, Windows Forms
Click event, 213-14
ClientRectangle property, 45
ClientSize property, 45
CreateGraphics method, 48
Invalidate method, 213
Load event, 45
Paint event, 417
SetStyle method, 157
Size property, 45
Update method, 213
Format method, String class, 112
FormatFlags property, StringFormat class, 107
forms, Windows Forms, 47
color depths, 47
pixel resolutions, 45
size, 47
FromArgb method, Color structure, 430
FromHDC method, Graphics class, 50
FromImage method, Graphics class, 463


Previous Section Next Section

Index

G

garbage collector, 33
GDI, 421-23
GDI+
coordinate system, 51-62
drawing surfaces, 37-63
namespaces, 16-17
GenericFontFamilies enumeration, 98
GenericTypography property, StringFormat class, 102
GetBounds method, GraphicsPath class, 180
GetBounds method, Region class, 185
GetBrightness method, Color structure, 42
GetCreateInstanceSupported method, TypeConverter class, 355-56
GetEditStyle method, UITypeEditor class, 371
GetFamilies method, FontFamily class, 114
GetFiles method, Directory class, 486-87
GetHdc method, Graphics class, 423
GetHue method, Color structure, 42
GetImageEncoders method, ImageCodecInfo class, 168
GetNearestColor method, Graphics class, 41
GetProperties method, TypeConverter class, 355
GetProperties method, TypeDescriptor class, 355
GetPropertiesSupported method, TypeConverter class, 355
GetRegionData method, Region class, 185
GetRegionScans method, Region class, 185
GetSaturation method, Color structure, 42
GetService method, IServiceProvider interface, 366
GetThumbnailImage method, Image class, 152-53
GIF format, images, 165
glyphs, 96-97
GotFocus event, Windows Forms controls, 317
Graphics class, 154, 463
Clear method, 48
Clip property, 198
ClipBounds property, 198
CompositingMode property, 158-60
DpiX property, 260
DpiY property, 260
DrawArc method, 48
DrawBezier method, 49
DrawCurve method, 49
DrawElipse method, 49
DrawIcon method, 49
DrawImage method, 463-65
DrawLine method, 119-20
DrawPath method, 49
DrawPie method, 469
DrawPolygon method, 49
DrawRectangle method, 280
DrawString method, 305, 463
ExcludeClip method, 197
FillClosedCurve method, 49
FillEllipse method, 49
FillPath method, 177
FillPie method, 469
FillPolygon method, 50
FillRectangle method, 297
FillRegion method, 50
FromHDC method, 50
FromImage method, 463
GetHdc method, 423
GetNearestColor method, 41
InterpolationMode property, 146-47
IntersectClip method, 197
IsClipEmpty property, 198
IsVisibleClipEmpty property, 198, 206-7
MeasureString method, 109
MultiplyTransform method, 235
PageUnit property, 255-56
ReleaseHdc method, 424
ResetClip method, 197
ResetTransform method, 226
RotateTransform method, 222
ScaleTransform method, 224
SetClip method, 201
overloads, 196-97
SmoothingMode property, 463
TextRenderingHint property, 463
Transform property, 233
TranslateClip method, 197
TranslateTransform method, 388
VisibleClipBounds property, 261
GraphicsOM class (example), 416
constructor, 277
Draw3DLine method, 85
Draw3DRectangle method, 285
DrawLine method, 279
DrawRectangle method, 279
FillRectangle method, 280
GraphicsPath class, 195
AddBezier method, 173
AddLine method, 178
AddString method, 195
ClearMarkers method, 180
Clone method, 180
CloseAllFigures method, 180
closed GraphicsPath objects, 174
CloseFigure method, 180
constructor, 180
FillMode property, 179
Flatten method, 180
GetBounds method, 180
IsOutlineVisible method, 180
IsVisible method, 180
open GraphicsPath objects, 174
PathData property, 179
PathPoints property, 179
PathTypes property, 179
PointCount property, 179
Reset method, 180
Reverse method, 180
SetMarkers method, 180
StartFigure method, 180
Transform method, 180
Warp method, 180
GraphicsUnit enumeration, 255-56
grid fitting, 102


Previous Section Next Section

Index

H

HasMorePages property, PrintPageEventArgs class, 249
HatchBrush class, 113
Hatchstyle enumeration, 89
Height property, Bitmap class, 142
Height property, Font class, 102
Hello World web service (example), 495-98
hit testing, 409-13
horizontal scrollbars, 399
horizontally aligned text, 107
HorizontalResolution property, Bitmap class, 142
hot spots, mouse cursors, 405
HotkeyPrefix property, StringFormat class, 316
HSB model, colors, 42
HScrollBar class, Windows Forms, 390
hue, 42


Previous Section Next Section

Index

I

IComponent Interface, 293
IDE compatibility, components, 292
identity matrix, 163
IDisposable interface, 32-35
Dispose method, 33-34
IIS (Internet Information Services), 446-50
documentation, 448-49
security, 449-50
virtual directories, 449
virtual roots, 449
Image class, 452
Clone method, 151
GetThumbnailImage method, 152-53
PixelFormat property, 138-41
RawFormat property, 454
Save method, 452
image URLs, 498-503
ImageAttributes class, 165
ImageButton controls, ASP.NET, 479
ImageClickEventArgs class, ASP.NET, 479
ImageCodecInfo class, 137
GetImageEncoders method, 168
images, 131-69
Alpha components, 162-65
animation, 165-67
ASP.NET, 446-69
bitmaps, 135-36
compression, 136
cloning an image, 151-52
color depths, 47
cropping an image, 148-49
decoders, 136-37
double-buffering, 156-57
dynamic images, 455-57
encoders, 136-37
interpolation, 146-48
pixel formats, 138-41
pixel resolutions, 46
pixels, 160-62
reflecting an image, 150
resizing, 146
resolution, 142-46
rotating an image, 150-51
scaling, 143-45
size, 142
skewing an image, 149-50
thumbnails, 481-84
vector images, 133
web services, 503-7
img tags, HTML, 455
immutable classes, 355-56
infinite regions, 187
Inflate method, Rectangle structure, 61
inheritance, components, 292
InstalledFontCollection class, 114
InstanceDescriptor class, 352
interfaces, components, 291
Internet Information Services (see IIS )
interpolation, images, 146-48
InterpolationMode enumeration, 146-47
InterpolationMode property, Graphics class, 146-47
Intersect method, Rectangle structure, 61
Intersect method, Region class, 184
IntersectClip method, Graphics class, 197
IntersectsWith method, Rectangle structure, 206-7
introspection, components, 292
Invalidate method, Windows Forms controls, 317
Invalidate method, Windows Forms Form class, 213
invalidation, 235
state, 209-10
IsClipEmpty property, Graphics class, 198
IsEmpty method, Region class, 185
IServiceProvider interface, 366
IsInfinite method, Region class, 185
IsMnemonic method, Windows Forms Control class, 319
IsOutlineVisible method, GraphicsPath class, 180
Isvisible method, GraphicsPath class, 180
Isvisible method, Region class, 185
IsVisibleClipEmpty property, Graphics class, 206-7
ITypeDescriptorContext interface, 365-66
IWindowsFormsEditorService interface, 366
DropDownControl method, 372
showDialog method, 366


Previous Section Next Section

Index

J

JPEG format, images, 167


Previous Section Next Section

Index

K

KeyDown event, Windows Forms controls, 326
KeyPress event, Windows Forms controls, 319
KeyUp event, Windows Forms controls, 319
Kind property, PrinterResolution class, 264


Previous Section Next Section

Index

L

Label controls, ASP.NET, 479
LandscapeAngle property, PrinterSettings class, 259
Language attribute, ASP.NET Control directives, 473
layering, 156
leadings, fonts, 119
licensing, components, 293
line caps, 72-74
LineAlignment property, StringFormat class, 315
linear transformations, 163
LinearGradientBrush class, 84-86
constructor, 84
SetInterpolationColors method, 86
LineCap enumeration, 72-73
LineJoin enumeration, 74
LineJoin property, Pen class, 74
lines, 51-53
dashed lines, 69-72
joining lines, 74-75
ListBox controls, ASP.NET, 461
Load event, Windows Forms Form class, 400
lossless compression, bitmaps, 136
lossy compression, bitmaps, 136
LostFocus event, Windows Forms controls, 317


Previous Section Next Section

Index

M

MakeEmpty method, Region class, 195
MakeInfinite method, Region class, 187
MapPath method, HttpServerUtility class, 464
MarginBounds property, PrintPageEventArgs class, 259
matrix algebra, 162
Matrix class, 234-35
MeasureString method, Graphics class, 109
Metafile class, 133
methods, components, 292
metrics, fonts, 115-21
metrics, printing, 258-67
MFC (Microsoft Foundation Classes), 15
Microsoft Foundation Classes (see MFC)
mnemonic keys, 318-19
mouse cursors, 405-9
custom mouse cursors, 408-9
hot spots, 405
mouse events, 409-21
wiggle-the-mouse bug, 433-42
MouseDown event, Windows Forms controls, 437
MouseEnter event, Windows Forms controls, 404
MouseEventArgs class, Windows Forms, 437
MouseHover event, Windows Forms controls, 404
MouseLeave event, Windows Forms controls, 404
MouseMove event, Windows Forms controls, 441
MouseUp event, Windows Forms controls, 439
Mousewheel event, Windows Forms controls, 404
multi-cast delegates, 322
multi-color linear gradients, 86
multi-line text, 105-7
multi-page printing, 251
MultiplyTransform method, Graphics class, 235


Previous Section Next Section

Index

N

namespaces, 27-30
Nativewindow class, Windows Forms, 420
.NET framework, 33
new line characters, 111
New Project dialog, Visual Studio .NET, 296
non-procedural constructs, Windows Forms, 338-39
non-scrollable controls, Windows Forms, 392-93
NumberRange structure (example), 350-51
NumberRangeConverter class (example), 351-53
NumberRangeDialog controls (example), 361-63
NumberRangeDropDown controls (example), 367-71
NumberRangeEditor controls (example), 371-72
NumberTextBoxA controls (example), 356-58
NumberTextBoxB controls (example), 360-61
NumericUpDown controls, Windows Forms, 328


Previous Section Next Section

Index

O

object oriented programming (see OOP )
Offset method, Point structure, 59
Offset method, Rectangle structure, 61
OnPaintAdornments method, Windows Forms ControlDesigner class, 379
OnSetCursor method, Windows Forms ControlDesigner class, 379
OOP (object oriented programming), 293
open GraphicsPath objects, 174
origin, coordinate systems, 62
Outline Model coordinate system (see also 272
3D effects, 282-86
Output window, Visual Studio .NET, 31
OutputStream property, HttpResponse class, 452
overhanging glyphs, 102


Previous Section Next Section

Index

P

packaging, components, 292
page coordinate system, 221
page transformations, 222
pagelets, ASP.NET, 473
PageSettings class, 263, 267
Bounds property, 259
Color property, 265-66
PaperSize property, 258
PrinterResolution property, 263-64
PrinterSettings property, 259
PageSettings property, PrintPageEventArgs class, 263
PageSettings property, Windows Forms PageSetupDialog class, 247
PageSetupDialog class, Windows Forms, 267
PageSettings property, 247
ShowDialog method, 247
PageUnit property, Graphics class, 255-56
Paint event, Windows Forms controls, 396
Paint event, Windows Forms Form class, 417
PaintEventArgs class, Windows Forms, 198
ClipRectangle property, 212
Panel class, Windows Forms, 389
AutoScroll property, 386
AutoScrollMinSize property, 386-87
AutoScrollPosition property, 388
PaperSize property, PageSettings class, 258
Param property, EncoderParameters class, 168
PathData property, GraphicsPath class, 179
PathGradientBrush class, 86-89
CenterColor property, 88
SurroundColors property, 88-89
PathPoints property, GraphicsPath class, 179
PathTypes property, GraphicsPath class, 179
Pen class, 283
Alignment property, 69
constructor, 66-67
CustomEndCap property, 74
CustomStartCap property, 74
DashPattern property, 71
DashStyle property, 71
Dispose method, 75
EndCap property, 72-73
LineJoin property, 74
StartCap property, 72-73
PenAlignment enumeration, 69
pens, 67-75
creating a pen, 90-91
width, 67-69
Pens class, 75-76
performance, 91-92
persistence, components, 292
picture galleries, 486-88
pie charts, 466-69
pixel formats, images, 138-41
pixel resolutions, 39
forms, 45
images, 46
printers, 46
PixelFormat enumeration, 139-41
PixelFormat property, Image class, 138-41
pixels, 273-76
clipping, 207-9
plugability, components, 292
PNG format, images, 135
Point cast, Size structure, 60
Point structure, 59-60
casts, 59
Ceiling method, 60
Offset method, 59
operator overloads, 59
Round method, 60
Truncate method, 60
PointCount property, GraphicsPath class, 179
PointF cast, Point structure, 59
PointF structure, 59-60
Print method, PrintDocument class, 242
print preview windows, 47
PrintDialog class, Windows Forms, 267
AllowSelection property, 249
AllowSomePages property, 252
ShowDialog method, 248
PrintDocument class, 266
BeginPrint event, 242
DefaultPageSettings property, 266
EndPrint event, 242
Print method, 242
PrinterSettings property, 261
PrintPage event, 267
QueryPageSettings event, 267
printer resolution, 40
PrinterResolution class, 264
PrinterResolution property, PageSettings class, 263-64
PrinterResolutionKind enumeration, 264
printers, 47
pixel resolutions, 46
PrinterSettings class, 241
LandscapeAngle property, 259
PrinterSettings property, PageSettings class, 259
PrinterSettings property, PrintDocument class, 261
printing, 239-68
metrics, 258-67
multi-page printing, 251
units of measurement, 254-58
PrintPage event, PrintDocument class, 267
PrintPageEventArgs class, 260-61
HasMorePages property, 249
MarginBounds property, 259
PageSettings property, 263
PrintPreviewDialog class, Windows Forms, 250
Document property, 243
ShowDialog method, 250
ProcessMnemonic method, Windows Forms controls, 325
properties, 347-48
components, 292
Windows Forms custom controls, 301-10
default values, 306-10
Properties window, Visual Studio .NET, 344-46, 348-58
composite properties, 345
properties, Windows Forms custom controls, 301-10


Previous Section Next Section

Index

Q

quality, text, 121-27
QueryPageSettings event, PrintDocument class, 267


Previous Section Next Section

Index

R

raster fonts, 97
raster operations, 422-23
raster-based drawing surfaces, 38
RawFormat property, Image class, 454
Rectangle structure, 193
!= operator, 61
== operator, 61
Contains method, 409
Inflate method, 61
Intersect method, 61
IntersectsWith method, 206-7
Offset method, 61
Union method, 61
RectangleF structure, 61
rectangles, 53-54
clipping retangles, 214
references, DLLs, 29-30
reflection transformations, 233-34
Region class, 410-11
Clone method, 185
Complement method, 184
constructor, 195
Exclude method, 184
GetBounds method, 185
GetRegionData method, 185
GetRegionScans method, 185
Intersect method, 184
IsEmpty method, 185
IsInfinite method, 185
Isvisible method, 185
MakeEmpty method, 195
MakeInfinite method, 187
Transform method, 185
Translate method, 185
Union method, 184
Xor method, 184
regions (see also Region class ), 181-87
clipping regions, 214-15
infinite regions, 187
set algebra, 183
ReleaseDC function, 424
ReleaseHdc method, Graphics class, 424
remote servers, 493
Reset method, GraphicsPath class, 180
ResetClip method, Graphics class, 197
ResetTransform method, Graphics class, 226
resizing, images, 146
resolution (see also 47
bitmaps, 145-46
fonts, 116-21
images, 142-46
printer resolution, 40
screen resolution, 40
resources, 32-33
Reverse method, GraphicsPath class, 180
right-justified tab stops, 112-13
RotateFlip method, Bitmap class, 465
RotateFlip Type enumeration, 465
RotateTransform method, Graphics class, 222
rotation transformations, 220, 29
Round method, Point structure, 60


Previous Section Next Section

Index

S

saturation, colors, 42
Save method, Bitmap class, 168
Save method, Image class, 452
ScaleTransform method, Graphics class, 224
scaling transformations, 223-24
scaling, images, 143-45
screen resolution, 40
ScrollableControl class, Windows Forms, 390-91
scrollbars, 396-99
ScrollEventArgs class, Windows Forms, 398
Type property, 398-99
scrolling, 385-401
drag-scrolling, 432-33
smooth scrolling, 393-401
security, components, 293
security, IIS, 449-50
server controls, ASP.NET, 471-90
design-time support, 488-90
thumbnail controls, 481-88
user controls, 473-81
set algebra, regions, 183
SetClip method, Graphics class, 201
overloads, 196-97
SetError method, Windows Forms ErrorProvider class, 332
SetInterpolationColors method, LinearGradientBrush class, 86
SetMarkers method, GraphicsPath class, 180
SetResolution method, Bitmap class, 145
SetStyle method, Windows Forms Form class, 157
SetTabStops method, StringFormat class, 110
shearing transformations, 221, 234-35
ShowDialog method, IWindowsFormsEditorService interface, 366
ShowDialog method, Windows Forms PageSetupDialog class, 247
ShowDialog method, Windows Forms PrintDialog class, 248
ShowDialog method, Windows Forms PrintPreviewDialog class, 250
Simple Object Access Protocol (see SOAP )
SimpleBlankControlDesigner class (example), 374-75
SimpleLineControl class (example), 377-78
SimpleLineControlDesigner class (example), 378-82
Size cast, Point structure, 59
Size property, Font class, 117
Size property, Windows Forms Form class, 45
Size structure, 60
operator overloads, 60
Point cast, 60
size, drawing surfaces, 47
forms, 47
images, 142
SizeF structure, 60
Sleep method, Thread class, 397, 439-40
smooth scrolling, 393-401
SmoothingMode enumeration, 55
SmoothingMode property, Graphics class, 463
SmoothScrollableControl class (example), 393-400
SOAP (Simple Object Access Protocol), 494
SolidBrush class, 76
Src attribute, ASP.NET Register directives, 474
src attribute, HTML img tags, 455
StartCap property, Pen class, 72-73
StartFigure method, GraphicsPath class, 180
state, 209-10
components, 292
Stream class, 452
streams, 452-53
StringFormat class, 315-16
Alignment property, 315
FormatFlags property, 107
GenericTypography property, 102
HotkeyPrefix property, 316
LineAlignment property, 315
SetTabStops method, 110
StringFormatFlags enumeration, 107
SurroundColors property, PathGradientBrush class, 88-89
System.Drawing namespace, 16
System.Drawing.Design namespace, 16
System.Drawing.dll file, 16
System.Drawing.Drawing2D namespace, 16
System.Drawing.Imaging namespace, 83
System.Drawing.Printing namespace, 16
System.Drawing.Text namespace, 16
SystemColors class, 283


Previous Section Next Section

Index

T

tab stops, 110-13
right-justified tab stops, 112-13
TagName attribute, ASP.NET Register directives, 474
TagPrefix attribute, ASP.NET Register directives, 474
text, 95-128
bounding rectangles, 101-3
ClearType anti-aliasing, 122-25
Font class, 99
font families, 116-17
font styles, 104-5
FontFamily class, 116-17
fonts, 114-27
metrics, 115-21
glyphs, 96-97
horizontally aligned text, 107
multi-line text, 105-7
new line characters, 111
quality, 121-27
StringFormat class, 110
tab stops, 110-13
typefaces, 97
vertically aligned text, 107
vertically oriented text, 108
TextBox controls, Windows Forms, 356
TextRenderingHint enumeration, 124-25
TextRenderingHint property, Graphics class, 463
TextureBrush class, 466
constructor, 77, 82
WrapMode property, 80-82
Thread class, 394
Sleep method, 439-40
ThreeDStyle enumeration (example), 284
thumbnail controls, ASP.NET, 481-88
thumbnails, images, 481-84
TIFF format, images, 135
ToInt32 method, Convert class, 483
toolbox icons, 488-89
Toolbox, Visual Studio .NET, 298-99
Transform method, GraphicsPath class, 180
Transform method, Region class, 185
Transform property, Graphics class, 233
transformations, 219-37
affine transformations, 222
custom controls, 236
page transformations, 222
reflection transformations, 233-34
rotation transformations, 220, 29
scaling transformations, 223-24
shearing transformations, 234-35
translation transformations, 228-29
world transformations, 222-35
Translate method, Region class, 185
TranslateClip method, Graphics class, 197
TranslateTransform method, Graphics class, 388
translation transformations, 228-29
TrueType fonts, 97
Truncate method, Point structure, 60
two-color linear gradients, 84
Type property, Windows Forms ScrollEventArgs class, 398-99
TypeConverter class, 352-53
CanConvertFrom method, 352
CanConvertTo method, 352
ConvertFrom method, 352
ConvertTo method, 352
CreateInstance method, 355-56
GetCreateInstanceSupported method, 355-56
GetProperties method, 355
GetPropertiesSupported method, 355
type-converter classes, 349
TypeDescriptor class, 355
typefaces, 97


Previous Section Next Section

Index

U

UDDI (Universal Description Discovery and Integration), 494
UITypeEditor class, 364
EditValue method, 371-72
GetEditStyle method, 371
Unicode, 96
Union method, Rectangle structure, 61
Union method, Region class, 184
units of measurement, 57-58
printing, 254-58
Universal Description Discovery and Integration (see UDDI )
unsafe code, 425-26
update method, Windows Forms Form class, 213
URLs, images, 498-503
user controls, ASP.NET, 473-81
properties, 475-76
user controls, Windows Forms, 294
UserControl class, Windows Forms, 394
using blocks, 34-35
using directives, 28
using keyword, 34


Previous Section Next Section

Index

V

validated event, Windows Forms controls, 340
Validating event, Windows Forms controls, 363
vector images, 133
vector-based drawing surfaces, 38
Verbs property, Windows Forms ControlDesigner class, 375
vertical scrollbars, 398
vertically aligned text, 107
vertically oriented text, 108
VerticalResolution property, Bitmap class, 142
viewports, 388-89
virtual directories, IIS, 449
virtual roots, IIS, 449
VisibleClipBounds property, Graphics class, 261
Visual Studio .NET, 17-18
Add Reference dialog, 29
Command Prompt window, 31-32
Design view, 346-47
New Project dialog, 21
Output window, 31
Properties window, 348-58
web applications, 450
web services, 498
Windows Forms custom controls, 295
vscrollBar class, Windows Forms, 390


Previous Section Next Section

Index

W

WaitCursor property, Windows Forms Cursors class, 408
Warp method, GraphicsPath class, 180
web applications, 450
Web Forms, 459
custom controls, 18-20
web methods, 504
web protocols, 493
web references, 506
web services, 493-507
image URLs, 498-503
images, 503-7
Web Services Description Language (see WSDL )
WebClient class, 500
WebMethod attribute, 495-96
widetext (see Unicode )
width property, Bitmap class, 142
width, pens, 67-69
wiggle-the-mouse bug, 433-42
Win32 API, 419-20
window decorations, 44
Windows applications, 30-32
Windows Forms (see also 44-45
custom controls, 289-341
non-scrollable controls, 392
scrolling, 385-401
windows handles, 420
windows procedures, 378
WndProc method, Windows Forms ControlDesigner class, 381
WndProc method, Windows Forms NativeWindow class, 420
world coordinate system, 221
world transformations, 222-35
WrapMode enumeration, 80-82
WrapMode property, TextureBrush class, 80-82
WSDL (Web Services Description Language), 496


Previous Section

Index

X

x heights, fonts, 115
Xor method, Region class, 184
Previous Section


Back Cover

Whether you are using Windows Forms to build rich-client business applications or the ASP.NET framework to build powerful web applications or web services, the use of well-designed graphics will greatly enhance their usability, impact, and visual appeal. This book provides a comprehensive guide to the use of GDI+ in .NET applications and aims to provide developers with all the information they need to build effective custom controls.

The opening section of the book investigates the .NET Framework classes that implement GDI+. It covers all of the classes, methods and techniques needed to create, manipulate, and display precise graphics in a form, a page being sent to a printer or an image.

On this foundation, the second section describes how to design and build effective custom controls for use in a business environment. Topics covered include building composite controls, implementing keyboard navigation, and enhancing design-time support.

The final section of the book explores the use of GDI+ and ASP.NET to build custom controls that can provide reusable, GUI components for web projects and to deliver customized graphics over the Internet.

About the Authors

Eric White is an independent software consultant with over 20 years experience in building management information systems, accounting systems, and other types of rich-client and n-tier database applications. He has written custom controls in numerous windowing systems, including all versions of Microsoft Windows since 2.0, Macintosh, OS/2, X/Motif, and others. Eric has particular interest in object-oriented design methodologies, including use case analysis, UML, and design patterns. After years of working with too many varieties of technologies to list, he is currently specializing in C#, Visual Basic .NET, ASP.NET, ADO.NET, XML, COM+, GDI+, SQL Server, and other Microsoft technologies.

Chris Garrett is the Technical Manager for one of the larger European new media agencies, working with some of the world’s best-known brands helping them develop web and interactive TV strategies.

Simon Robinson lives in Lancaster, UK, where he shares a house with some students. He first encountered serious programming when he was doing his PhD in physics, modeling all sorts of weird things to do with superconductors and quantum mechanics. The experience of programming was nearly enough to put him off computers for life (though, oddly, he seems to have survived all the quantum mechanics), and for a while he tried being a sports massage therapist instead. But he then realized how much more money was in computers compared to sports massage, and rapidly got a job as a C++ programmer and researcher instead.