349 36 7MB
English Pages xxx, 972 pages: illustrations [1006] Year 2005;2006
CORE C# AND .NET
PRENTICE HALL CORE SERIES Core J2EE Patterns, Second Edition, Alur/Malks/Crupi Core PHP Programming,Third Edition, Atkinson/Suraski Core Lego Mindstorms, Bagnall Core JSTL, Geary Core JavaServer Faces, Geary/Horstmann Core Web Programming, Second Edition, Hall/Brown Core Servlets and JavaServer Pages, Second Edition, Hall/Brown Core Java™ 2, Volume I—Fundamentals, Horstmann/Cornell Core Java™ 2, Volume II—Advanced Features, Horstmann/Cornell Core C# and .NET, Perry Core CSS, Second Edition, Schengili-Roberts Core Security Patterns, Steel/Nagappan/Lai Core Java Data Objects, Tyagi/Vorburger/ McCammon/Bobzin Core Web Application Development with PHP and MySQL, Wandschneider
CORE C# AND .NET
Stephen C. Perry
Prentice Hall Professional Technical Reference Upper Saddle River, NJ • Boston • Indianapolis • San Francisco New York • Toronto • Montreal • London • Munich • Paris • Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals. The author and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special sales, which may include electronic versions and/or custom covers and content particular to your business, training goals, marketing focus, and branding interests. For more information, please contact: U. S. Corporate and Government Sales (800) 382-3419 [email protected] For sales outside the U. S., please contact: International Sales [email protected] Visit us on the Web: www.phptr.com Library of Congress Cataloging-in-Publication Data Perry, Stephen (Stephen C.) Core C# and .NET / Stephen Perry. p. cm. ISBN 0-13-147227-5 1. C++ (Computer program language) 2. Microsoft .NET. I. Title. QA76.73.C153P468 2005 005.13'3--dc22 2005021301 Copyright © 2006 Pearson Education, Inc. All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, write to: Pearson Education, Inc. Rights and Contracts Department One Lake Street Upper Saddle River, NJ 07458 ISBN 0-13-147227-5 Text printed in the United States on recycled paper at R. R. Donnelley in Crawfordsville, Indiana. First printing, September 2005
Chapter
ABOUT THE AUTHOR FOREWORD XXV PREFACE XXVII ACKNOWLEDGMENTS
XXIII
XXX
Part I FUNDAMENTALS OF C# PROGRAMMING AND INTRODUCTION TO .NET 2
C ha p t er 1 INTRODUCTION TO .NET AND C# 1.1
Overview of the .NET Framework
4 6
Microsoft .NET and the CLI Standards 1.2
Common Language Runtime
1.3
9
Compiling .NET Code
10
Common Type System
11
Assemblies
7
13
Framework Class Library
18 v
vi
Contents
1.4
Working with the .NET Framework and SDK Updating the .NET Framework .NET Framework Tools Ildasm.exe
25
wincv.exe
28
23
23
Framework Configuration Tool 1.5
29
Understanding the C# Compiler Locating the Compiler
31
31
Compiling from the Command Line 1.6
Summary
1.7
Test Your Understanding
22
32
35 36
Chapter 2 C# LANGUAGE FUNDAMENTALS 2.1
The Layout of a C# Program
38 40
General C# Programming Notes 2.2
Primitives
42
45
decimal
47
bool
47
char
48
byte, sbyte
48
short, int, long single, double
48 49
Using Parse and TryParse to Convert a Numeric String 2.3
Operators: Arithmetic, Logical, and Conditional Arithmetic Operators
50
Conditional and Relational Operators Control Flow Statements
2.4
if-else
53
switch
54
Loops
55
while loop
55
52
51
50
49
Contents
do loop
56
for loop
56
foreach loop
57
Transferring Control Within a Loop 2.5
C# Preprocessing Directives
59
Conditional Compilation
60
Diagnostic Directives Code Regions 2.6
Strings
60
61
61
String Literals
61
String Manipulation 2.7
Enumerated Types
63
66
Working with Enumerations System.Enum Methods Enums and Bit Flags 2.8
Arrays
58
66
68 69
69
Declaring and Creating an Array
70
Using System.Array Methods and Properties 2.9
Reference and Value Types
71
73
System.Object and System.ValueType
73
Memory Allocation for Reference and Value Types Boxing
75
Summary of Value and Reference Type Differences 2.10
Summary
2.11
Test Your Understanding
78 78
Chapter 3 CLASS DESIGN IN C#
80
3.1
Introduction to a C# Class
3.2
Defining a Class Attributes
74
82 83
Access Modifiers
85
82
77
vii
viii
Contents
Abstract, Sealed, and Static Modifiers Class Identifier
86
Base Classes, Interfaces, and Inheritance 3.3
Overview of Class Members
Fields
91 93
Indexers Methods
95 97
Method Modifiers Passing Parameters 3.6
Constructors
98 103
106
Instance Constructor Private Constructor Static Constructor 3.7
89
89
Properties 3.5
89
Constants, Fields, and Properties Constants
Delegates and Events Delegates
106 110 111 112
113
Delegate-Based Event Handling 3.8
Operator Overloading
3.9
Interfaces
115
123
126
Creating and Using a Custom Interface Working with Interfaces 3.10
Generics
3.11
Structures
87
88
Member Access Modifiers 3.4
86
127
129
131 134
Defining Structures
134
Using Methods and Properties with a Structure 3.12
Structure Versus Class
136
137
Structures Are Value Types and Classes Are Reference Types Unlike a Class, a Structure Cannot Be Inherited
138
138
General Rules for Choosing Between a Structure and a Class
139
Contents
3.13
Summary
139
3.14
Test Your Understanding
140
Chapter 4 WORKING WITH OBJECTS IN C# 4.1
Object Creation
144
145
Example: Creating Objects with Multiple Factories 4.2
Exception Handling
148
149
System.Exception Class
150
Writing Code to Handle Exceptions
151
Example: Handling Common SystemException Exceptions How to Create a Custom Exception Class Unhandled Exceptions
157
Exception Handling Guidelines 4.3
159
Implementing System.Object Methods in a Custom Class ToString() to Describe an Object Equals() to Compare Objects
163
168
System.Collections Namespace Stack and Queue ArrayList
179
Hashtable
181
177
177
System.Collections.Generic Namespace 4.5
Object Serialization
187
Binary Serialization 4.6
165
Working with .NET Collection Classes and Interfaces Collection Interfaces
188
Object Life Cycle Management .NET Garbage Collection
4.7
Summary
4.8
Test Your Understanding
192 192
198 198
160
161
Cloning to Create a Copy of an Object 4.4
155
184
167
153
ix
x
Contents
Par t II CREATING APPLICATIONS USING THE .NET FRAMEWORK CLASS LIBRARY 200
Chapter 5 C# TEXT MANIPULATION AND FILE I/O 5.1
Characters and Unicode Unicode
204
204
Working with Characters 5.2
The String Class
205
209
Creating Strings
209
Overview of String Operations 5.3
Comparing Strings
211
212
Using String.Compare
213
Using String.CompareOrdinal 5.4
202
215
Searching, Modifying, and Encoding a String’s Content Searching the Contents of a String
216
Searching a String That Contains Surrogates String Transformations String Encoding 5.5
StringBuilder
217
219
220
StringBuilder Class Overview
221
StringBuilder Versus String Concatenation 5.6
5.7
217
Formatting Numeric and DateTime Values Constructing a Format Item
224
Formatting Numeric Values
225
Formatting Dates and Time
227
Regular Expressions
232
The Regex Class
232
Creating Regular Expressions A Pattern Matching Example
237 239
222 223
216
Contents
Working with Groups
240
Examples of Using Regular Expressions 5.8
242
System.IO: Classes to Read and Write Streams of Data The Stream Class FileStreams
244
244
245
MemoryStreams
247
BufferedStreams
248
Using StreamReader and StreamWriter to Read and Write Lines of Text 249 StringWriter and StringReader
251
Encryption with the CryptoStream Class 5.9
System.IO: Directories and Files FileSystemInfo
252
255
256
Working with Directories Using the DirectoryInfo, Directory, and Path Classes 256 Working with Files Using the FileInfo and File Classes 5.10
Summary
263
5.11
Test Your Understanding
264
Chapter 6 BUILDING WINDOWS FORMS APPLICATIONS 6.1
Programming a Windows Form
268
Building a Windows Forms Application by Hand 6.2
Windows.Forms Control Classes The Control Class Control Events
6.3
The Form Class
271
272
Working with Controls
274
279 285
Setting a Form’s Appearance
286
Setting Form Location and Size Displaying Forms
266
290
292
The Life Cycle of a Modeless Form
292
268
261
xi
xii
Contents
Forms Interaction—A Sample Application Owner and Owned Forms
298
Message and Dialog Boxes
299
Multiple Document Interface Forms 6.4
Working with Menus Context Menus
306
307
Adding Help to a Form ToolTips
301
306
MenuItem Properties 6.5
308
309
Responding to F1 and the Help Button The HelpProvider Component 6.6
294
Forms Inheritance
311
312
313
Building and Using a Forms Library Using the Inherited Form 6.7
Summary
6.8
Test Your Understanding
313
314
315 316
Chapter 7 WINDOWS FORMS CONTROLS
318
7.1
A Survey of .NET Windows Forms Controls
319
7.2
Button Classes, Group Box, Panel, and Label
323
The Button Class
323
The CheckBox Class
324
The RadioButton Class The GroupBox Class
7.3
327
The Panel Class
328
The Label Class
330
PictureBox and TextBox Controls The PictureBox Class The TextBox Class
7.4
325
331
331 333
ListBox, CheckedListBox, and ComboBox Classes The ListBox Class
335
335
Contents
Other List Controls: the ComboBox and the CheckedListBox 7.5
The ListView and TreeView Classes The ListView Class
342
The TreeView Class 7.6
349
The ProgressBar, Timer, and StatusStrip Classes Building a StatusStrip
7.7
342
355
355
Building Custom Controls
358
Extending a Control
358
Building a Custom UserControl A UserControl Example
359
359
Using the Custom User Control
361
Working with the User Control at Design Time 7.8
Using Drag and Drop with Controls Overview of Drag and Drop
7.9
Using Resources
363
363
369
Working with Resource Files
369
Using Resource Files to Create Localized Forms 7.10
Summary
7.11
Test Your Understanding
376 376
Chapter 8 .NET GRAPHICS USING GDI+ 8.1
GDI+ Overview
378
380
The Graphics Class The Paint Event 8.2
380 384
Using the Graphics Object Basic 2-D Graphics Pens Brushes Colors
362
388
388
393 395 400
A Sample Project: Building a Color Viewer
402
373
341
xiii
xiv
Contents
8.3
Images
407
Loading and Storing Images Manipulating Images
408
411
Sample Project: Working with Images A Note on GDI and BitBlt for the Microsoft Windows Platform 8.4
Summary
8.5
Test Your Understanding
414
421
423 423
Chapter 9 FONTS, TEXT, AND PRINTING 9.1
Fonts
428
Font Families
428
The Font Class 9.2
426
430
Drawing Text Strings
433
Drawing Multi-Line Text
434
Formatting Strings with the StringFormat Class Using Tab Stops
436
String Trimming, Alignment, and Wrapping 9.3
Printing
438
439
Overview
439
PrintDocument Class Printer Settings Page Settings
441
442 445
PrintDocument Events PrintPage Event
446
448
Previewing a Printed Report A Report Example
449
450
Creating a Custom PrintDocument Class 9.4
Summary
457
9.5
Test Your Understanding
458
454
435
Contents
Chapter 10 WORKING WITH XML IN .NET 10.1
Working with XML
460
462
Using XML Serialization to Create XML Data XML Schema Definition (XSD) Using an XML Style Sheet 10.2
466
468
Techniques for Reading XML Data XmlReader Class
462
472
472
XmlNodeReader Class
477
The XmlReaderSettings Class
479
Using an XML Schema to Validate XML Data Options for Reading XML Data
481
10.3
Techniques for Writing XML Data
10.4
Using XPath to Search XML
XmlDocument and XPath
486 489
XPathDocument and XPath
490
XmlDataDocument and XPath Summary
10.6
Test Your Understanding
482
485
Constructing XPath Queries
10.5
480
491
493 494
Chapter 11 ADO.NET 11.1
496 Overview of the ADO.NET Architecture OLE DB Data Provider in .NET .NET Data Provider
11.2
498
499
Data Access Models: Connected and Disconnected Connected Model Disconnected Model
11.3
498
502 504
ADO.NET Connected Model Connection Classes
506
506
502
xv
xvi
Contents
The Command Object DataReader Object 11.4
511 516
DataSets, DataTables, and the Disconnected Model The DataSet Class DataTables
518
518
519
Loading Data into a DataSet
523
Using the DataAdapter to Update a Database
525
Defining Relationships Between Tables in a DataSet
530
Choosing Between the Connected and Disconnected Model 11.5
XML and ADO.NET
532
533
Using a DataSet to Create XML Data and Schema Files Creating a DataSet Schema from XML Reading XML Data into a DataSet 11.6
Summary
11.7
Test Your Understanding
534
536
537
540 541
ChapteR 12 DATA BINDING WITH WINDOWS FORMS CONTROLS 12.1
Overview of Data Binding Simple Data Binding
546
546
Complex Data Binding with List Controls One-Way and Two-Way Data Binding Using Binding Managers 12.2
550
552 555
Binding Controls to an ArrayList Adding an Item to the Data Source Identifying Updates
558 560
561
Update Original Database with Changes 12.3
549
Using Simple and Complex Data Binding in an Application Binding to a DataTable
The DataGridView Class Properties
564
544
563
562
555
Contents
Events
571
Setting Up Master-Detail DataGridViews Virtual Mode
576
579
12.4
Summary
585
12.5
Test Your Understanding
585
Part III ADVANCED USE OF C# AND THE .NET FRAMEWORK
Chapter 13 ASYNCHRONOUS PROGRAMMING AND MULTITHREADING 590 13.1
What Is a Thread? Multithreading
13.2
592 592
Asynchronous Programming
595
Asynchronous Delegates
596
Examples of Implementing Asynchronous Calls 13.3
Working Directly with Threads
609
Creating and Working with Threads Multithreading in Action
613
Using the Thread Pool Timers 13.4
617
618
Thread Synchronization
620
The Synchronization Attribute The Monitor Class The Mutex
609
622
623
625
The Semaphore Avoiding Deadlock
627 628
Summary of Synchronization Techniques 13.5
Summary
631
13.6
Test Your Understanding
631
630
599
588
xvii
xviii
Contents
Chapter 14 CREATING DISTRIBUTED APPLICATIONS WITH REMOTING 14.1
Application Domains
636
638
Advantages of AppDomains
14.2
638
Application Domains and Assemblies
639
Working with the AppDomain Class
640
Remoting
643
Remoting Architecture Types of Remoting
644
648
Client-Activated Objects
650
Server-Activated Objects
650
Type Registration
652
Remoting with a Server-Activated Object
654
Remoting with a Client-Activated Object (CAO)
664
Design Considerations in Creating a Distributed Application 14.3
Leasing and Sponsorship Leasing
671
672
Sponsorship
675
14.4
Summary
678
14.5
Test Your Understanding
678
Chapter 15 CODE REFINEMENT, SECURITY, AND DEPLOYMENT 15.1
Following .NET Code Design Guidelines Using FxCop
15.2
683
Strongly Named Assemblies
686
Creating a Strongly Named Assembly Delayed Signing
688
Global Assembly Cache (GAC) Versioning
682
690
689
687
680
670
Contents
15.3
Security
692
Permissions and Permission Sets Evidence
693
698
Security Policies
701
Configuring Security Policy
702
The .NET Framework Configuration Tool
704
Configuring Code Access Security with the Configuration Tool—An Example 706 Requesting Permissions for an Assembly Programmatic Security 15.4
711
715
Application Deployment Considerations
722
Microsoft Windows Deployment: XCOPY Deployment Versus the Windows Installer
722
Deploying Assemblies in the Global Assembly Cache Deploying Private Assemblies Using CodeBase Configuration
723
724 725
Using a Configuration File to Manage Multiple Versions of an Assembly 726 Assembly Version and Product Information 15.5
Summary
15.6
Test Your Understanding
727
728 728
Part IV PROGRAMMING FOR THE INTERNET
730
Chapter 16 ASP.NET WEB FORMS AND CONTROLS 16.1
732
Client-Server Interaction over the Internet
734
Web Application Example: Implementing a BMI Calculator Using ASP.NET to Implement a BMI Calculator Inline Code Model
741
740
735
xix
xx
Contents
The Code-Behind Model
749
Code-Behind with Partial Classes Page Class 16.2
753
754
Web Forms Controls
758
Web Controls Overview
759
Specifying the Appearance of a Web Control Simple Controls List Controls
761 766
The DataList Control 16.3
768
Data Binding and Data Source Controls Binding to a DataReader Binding to a DataSet Validation Controls
774 776
784
Using Validation Controls 16.5
772
772
DataSource Controls 16.4
760
786
Master and Content Pages
789
Creating a Master Page
790
Creating a Content Page
791
Accessing the Master Page from a Content Page 16.6
Building and Using Custom Web Controls A Custom Control Example Using a Custom Control
794 796
Control State Management Composite Controls
793
797
798
16.7
Selecting a Web Control to Display Data
16.8
Summary
16.9
Test Your Understanding
801
802 803
Chapter 17 THE ASP.NET APPLICATION ENVIRONMENT 17.1
HTTP Request and Response Classes HttpRequest Object
808
808
806
792
Contents
HttpResponse Object 17.2
813
ASP.NET and Configuration Files A Look Inside web.config
817
818
Adding a Custom Configuration Section 17.3
ASP.NET Application Security Forms Authentication
827
827
An Example of Forms Authentication 17.4
Maintaining State
17.5
Caching
837
838
841
Page Output Caching Data Caching 17.6
830
835
Application State Session State
824
842
845
Creating a Web Client with WebRequest and WebResponse WebRequest and WebResponse Classes Web Client Example
17.7
HTTP Pipeline
848
848
851
Processing a Request in the Pipeline HttpApplication Class HTTP Modules
857
HTTP Handlers
862
17.8
Summary
17.9
Test Your Understanding
851
853
866 867
Chapter 18 XML WEB SERVICES 18.1
868
Introduction to Web Services
870
Discovering and Using a Web Service 18.2
Building an XML Web Service
871
875
Creating a Web Service by Hand
875
Creating a Web Service Using VS.NET
878
848
xxi
xxii
Contents
Extending the Web Service with the WebService and WebMethod Attributes 18.3
Building an XML Web Service Client
880
884
Creating a Simple Client to Access the Web Service Class Creating a Proxy with Visual Studio.NET 18.4
Understanding WSDL and SOAP
894
895
Web Services Description Language (WSDL) Simple Object Access Protocol (SOAP) 18.5
895
898
Using Web Services with Complex Data Types A Web Service to Return Images Using Amazon Web Services
906
907
909
Creating a Proxy for the Amazon Web Services Building a WinForms Web Service Client 18.6
Web Services Performance
916
Configuring the HTTP Connection
916
Working with Large Amounts of Data 18.7
Summary
18.8
Test Your Understanding
913
917
918 918
Appendix A FEATURES SPECIFIC TO .NET 2.0 AND C# 2.0
920
Appendix B DATAGRIDVIEW EVENTS AND DELEGATES ANSWERS TO CHAPTER EXERCISES INDEX
952
938
924
911
884
Chapter
Stephen Perry is a software architect specializing in the design and implementation of .NET applications. For the past three years he has designed and developed significant .NET-based solutions for clients in the textile, furniture, legal, and medical professions. Prior to that, he worked for more than 20 years in all phases of software development. With the extra 25 hours a week now available from the completion of the book, he’ll have more time for triathlon training and watching “Seinfeld” reruns.
xxiii
This page intentionally left blank
Chapter
Learning a new programming language, or even a new version of one, can be a lot like traveling to a foreign country. Some things will look familiar. Some things will look very odd. You can usually get by if you stick to the familiar; but, who wants to just “get by”? Often times, it’s the odd things in a country, culture, or language where you can realize many interesting and valuable benefits. To do it right, however, you’ll need a proper guide or guide book. This is essential. Just as a good guide book will tell you what to see, when to see it, what to eat, what to avoid, and a few tips, so will a good programming book tell you what frameworks and classes to use, how to call their methods, what bad practices to avoid, and a few productivity tips and best practices. This is exactly what Steve has provided you. If you were to approach C# 2.0 from a 1.0 perspective, or from some other language background, you’d be missing out on all its new offerings. For example, I’m a seasoned developer and set in my ways. If you’re like me, then you probably still write methods, loops, and other design patterns the same way you have for many years. You know it’s not producing the most efficient code, but it’s quick. It’s easy to read and understand; and it works. Steve has literally written the “Core” C# book here. He begins by introducing you to the important C# concepts and their interaction with the .NET Framework; but, then he deviates from the other C# reference books on the market and jumps right into the application of C#. These include most of the common, daily tasks that you could be asked to perform, such as working with text, files, databases, XML, Windows forms and controls, printing, ASP.NET Web applications, Web services, and
xxv
xxvi
Foreword
remoting. Steve even provides you with asynchronous, multithreaded, security, and deployment topics as a bonus. You won’t need another book on your shelf. So, what are you waiting for? I think it’s time to break a few of your familiar habits and experience some new culture!
Richard Hundhausen Author, Introducing Microsoft Visual Studio 2005 Team System
Chapter
“The process of preparing programs for a digital computer is especially attractive because it not only can be economically and scientifically rewarding, it can also be an aesthetic experience much like composing poetry or music.” — Donald Knuth, Preface to Fundamental Algorithms (1968)
Thirty-seven years later, programmers still experience the same creative satisfaction from developing a well-crafted program. It can be 10 lines of recursive code that pops into one’s head at midnight, or it can be an entire production management system whose design requires a year of midnights. Then, as now, good programs still convey an impression of logic and naturalness—particularly to their users. But the challenges have evolved. Software is required to be more malleable—it may be run from a LAN, the Internet, or a cellular phone. Security is also a much bigger issue, because the code may be accessible all over the world. This, in turn, raises issues of scalability and how to synchronize code for hundreds of concurrent users. More users bring more cultures, and the concomitant need to customize programs to meet the language and culture characteristics of a worldwide client base. .NET—and the languages written for it—addresses these challenges as well as any unified development environment. This book is written for developers, software architects, and students who choose to work with the .NET Framework. All code in the book is written in C#, although only one chapter is specifically devoted to the syntactical structure of the C# language. This book is not an introduction to programming—it assumes you are experienced in a computer language. This book is not an introduction to object-oriented programxxvii
xxviii
Preface
ming (OOP)—although it will re-enforce the principles of encapsulation, polymorphism, and inheritance through numerous examples. Finally, this book is not an introduction to using Visual Studio.NET to develop C# programs. VS.NET is mentioned, but the emphasis is on developing and understanding C# and the .NET classes—independent of any IDE. This book is intended for the experienced programmer who is moving to .NET and wants to get an overall feel for its capabilities. You may be a VB6 or C++ programmer seeking exposure to .NET; a VB.NET programmer expanding your repertoire into C#; or—and yes it does happen occasionally—a Java programmer investigating life on the far side. Here’s what you’ll find if you choose to journey through this book. •
•
•
•
18 Chapters. The first four chapters should be read in order. They provide an introduction to C# and a familiarity with using the .NET class libraries. The remaining chapters can be read selectively based on your interests. Chapters 6 and 7 describe how to develop Windows Forms applications. Chapters 8 and 9 deal with GDI+—the .NET graphics classes. Chapters 10 through 12 are about working with data. Both XML and ADO.NET are discussed. Chapters 13, 14, and 15 tackle the more advanced topics of threading, remoting, and code security, respectively. The final chapters form a Web trilogy: Chapter 16 discusses ASP.NET Web page development; Chapter 17 looks behind the scenes at how to manage state information and manage HTTP requests; the book closes with a look at Web Services in Chapter 18. .NET 2.0. The manuscript went to publication after the release of Beta 2.0. As such, it contains information based on that release. The 2.0 topics are integrated within the chapters, rather than placing them in a special 2.0 section. However, as a convenience, Appendix A contains a summary and separate index to the .NET 2.0 topics. Coding examples. Most of the code examples are short segments that emphasize a single construct or technique. The objective is to avoid filler code that does nothing but waste paper. Only when it is essential does a code example flow beyond a page in length. Note that all significant code examples are available as a download from www.corecsharp.net or indirectly at www.phptr.com/title/ 0131472275. To access the download area, enter the keyword parsifal. Questions and answers. Each chapter ends with a section of questions to test your knowledge. The answers are available in a single section at the end of the book.
Preface
•
Fact rather than opinion. This book is not based on my opinion; it is based on the features inherent in .NET and C#. Core recommendations and notes are included with the intent of providing insight rather than opinion.
Although some will disagree, if you really want to learn C# and .NET, shut down your IDE, pull out your favorite text editor, and learn how to use the C# compiler from the command line. After you have mastered the fundamentals, you can switch to VS.NET and any other IDE for production programming. Finally, a word about .NET and Microsoft: This book was developed using Microsoft .NET 1.x and Whidbey betas. It includes topics such as ADO.NET and ASP.NET that are very much Microsoft proprietary implementations. In fact, Microsoft has applied to patent these methodologies. However, all of C# and many of the .NET basic class libraries are based on a standard that enables them to be ported to other platforms. Now, and increasingly in the future, many of the techniques described in this book will be applicable to .NET like implementations (such as the Mono project, http://www.mono-project.com/Main_Page) on non-Windows platforms.
xxix
Chapter
I have received assistance from a great number of people over the 21 months that went into the research and development of this book. I wish to thank first my wife, Rebecca, who tirelessly read through pages of unedited manuscripts, and used her systems programming background to add valuable recommendations. Next, I wish to thank the reviewers whose recommendations led to better chapter organization, fewer content and code errors, and a perspective on which topics to emphasize. Reviewers included Greg Beamer, James Edelen, Doug Holland, Curtiss Howard, Anand Narayanaswamy, and Gordon Weakliem. Special thanks go to Richard Hundhausen whose recommendations went well beyond the call of duty; and Cay Horstmann, who read every preliminary chapter and whose Java allegiance made him a wonderful and influential “Devil’s Advocate.” I also wish to thank Dr. Alan Tharp who encouraged the idea of writing a book on .NET and remains my most respected advisor in the computer profession. Finally, it has been a pleasure working with the editors and staff at Prentice Hall PTR. I’m particularly grateful for the efforts of Stephane Nakib, Joan Murray, Ebony Haight, Jessica D’Amico, Kelli Brooks, and Vanessa Moore. This book would not exist without the efforts of my original editor Stephane Nakib. The idea for the book was hers, and her support for the project kept it moving in the early stages. My other editor, Joan Murray, took over midway through the project and provided the oversight, advice, and encouragement to complete the project. Production editor Vanessa Moore and copy editor Kelli Brooks performed the “dirty work” of turning the final manuscript—with its myriad inconsistencies and word misuse—into a presentable book. To them, I am especially grateful. Working with professionals such as these was of inestimable value on those days when writing was more Sisyphean than satisfying. xxx
This page intentionally left blank
FUNDAMENTALS OF C# PROGRAMMING AND INTRODUCTION TO
.NET
I ■
■
■
■
Chapter 1 Introduction to .NET and C#
4
Chapter 2 C# Language Fundamentals
38
Chapter 3 Class Design in C#
80
Chapter 4 Working with Objects in C#
144
INTRODUCTION TO .NET AND C#
Topics in This Chapter • Overview of the .NET Framework: Architecture and features. • Common Language Runtime: An overview of the tasks performed by the runtime portion of the .NET Framework: Just-in-Time (JIT) compiler, loading assemblies, and code verification. • Common Type System and Common Language Specifications: Rules that govern Common Language Runtime (CLR) compatibility and language interoperability. • Assemblies: A look at the structure of an assembly, the philosophy behind it, and the difference between private and shared assemblies. • Framework Class Library: The Framework Library supplies hundreds of base classes grouped into logical namespaces. • Development Tools: Several tools are provided with .NET to aid code development. These include Ildasm for disassembling code, WinCV to view the properties of a class, and the Framework Configuration tool. • Compiling and Running C# Programs: Using the C# compiler from the command line and options for structuring an application.
1
The effective use of a language requires a good deal more than learning the syntax and features of the language. In fact, the greater part of the learning curve for new technology is now concentrated in the programming environment. It is not enough to be proficient with the C# language; the successful developer and software architect must also be cognizant of the underlying class libraries and the tools available to probe these libraries, debug code, and check the efficiency of underlying code. The purpose of this chapter is to provide an awareness of the .NET environment before you proceed to the syntax and semantics of the C# language. The emphasis is on how the environment, not the language, will affect the way you develop software. If you are new to .NET, it is necessary to embrace some new ideas. .NET changes the way you think of dealing with legacy code and version control; it changes the way program resources are disposed of; it permits code developed in one language to be used by another; it simplifies code deployment by eliminating a reliance on the system registry; and it creates a self-describing metalanguage that can be used to determine program logic at runtime. You will bump into all of these at some stage of the software development process, and they will influence how you design and deploy your applications. To the programmer’s eye, the .NET platform consists of a runtime environment coupled with a base class library. The layout of this chapter reflects that viewpoint. It contains separate sections on the Common Language Runtime (CLR) and the Framework Class Library (FCL). It then presents the basic tools that a developer may use to gain insight into the inner workings of the .NET Framework, as well as manage and distribute applications. As a prelude to Chapter 2, the final section introduces the C# compiler with examples of its use. 5
6
Chapter 1
1.1
■
Introduction to .NET and C#
Overview of the .NET Framework
The .NET Framework is designed as an integrated environment for seamlessly developing and running applications on the Internet, on the desktop as Windows Forms, and even on mobile devices (with the Compact Framework). Its primary objectives are as follows: • •
•
•
To provide a consistent object-oriented environment across the range of applications. To provide an environment that minimizes the versioning conflicts (“DLL Hell”) that has bedeviled Windows (COM) programmers, and to simplify the code distribution/installation process. To provide a portable environment, based on certified standards, that can be hosted by any operating system. Already, C# and a major part of the .NET runtime, the Common Language Infrastructure (CLI), have been standardized by the ECMA.1 To provide a managed environment in which code is easily verified for safe execution.
To achieve these broad objectives, the .NET Framework designers settled on an architecture that separates the framework into two parts: the Common Language Runtime (CLR) and the Framework Class Library (FCL). Figure 1-1 provides a stylized representation of this.
FRAMEWORK CLASS LIBRARY Windows Forms
Web Applications ASP.NET, Web Services
Data Classes ADO.NET, XML, SQL Base Classes System.IO, System.Drawing, System.Threading Common Language Runtime CTS, Just-in-Time Compiler, Memory Management
Operating System Figure 1-1 .NET Framework
1. ECMA International was formerly known as the European Computer Manufacturers Association and is referred to herein simply as the ECMA.
1.1 Overview of the .NET Framework
The CLR—which is Microsoft’s implementation of the CLI standard—handles code execution and all of the tasks associated with it: compilation, memory management, security, thread management, and enforcement of type safety and use. Code that runs under the CLR is referred to as managed code. This is to distinguish it from unmanaged code that does not implement the requirements to run in the CLR— such as COM or Windows API based components. The other major component, the Framework Class Library, is a reusable code library of types (classes, structures, and so on) available to applications running under .NET. As the figure shows, these include classes for database access, graphics, interoperating with unmanaged code, security, and both Web and Windows forms. All languages that target the .NET Framework use this common class library. Thus, after you gain experience using these types, you can apply that knowledge to any .NET language you may choose to program in.
Microsoft .NET and the CLI Standards A natural concern for a developer that chooses to invest the time in learning C# and .NET is whether the acquired skill set can be transferred to other platforms. Specifically, is .NET a Microsoft product tethered only to the Windows operating system? Or is it a portable runtime and development platform that will be implemented on multiple operating systems? To answer the question, it is necessary to understand the relationship among Microsoft .NET, C#, and the Common Language Infrastructure (CLI) standards. The CLI defines a platform-independent virtual code execution environment. It specifies no operating system, so it could just as easily be Linux as Windows. The centerpiece of the standard is the definition for a Common Intermediate Language (CIL) that must be produced by CLI compliant compilers and a type system that defines the data types supported by any compliant language. As described in the next section, this intermediate code is compiled into the native language of its host operating system. The CLI also includes the standards for the C# language, which was developed and promoted by Microsoft. As such, it is the de facto standard language for .NET. However, other vendors have quickly adopted the CIL standard and produced—just to name a few—Python, Pascal, Fortran, Cobol, and Eiffel .NET compilers. The .NET Framework, as depicted in Figure 1-1, is Microsoft’s implementation of the CLI standards. The most important thing to note about this implementation is that it contains a great deal more features than are specified by the CLI architecture. To illustrate this, compare it to the CLI standards architecture shown in Figure 1-2.
7
8
Chapter 1
■
Introduction to .NET and C#
Network Library
XML Library
Reflection Library
Runtime Infrastructure Library Base Class Library Kernel Profile Compact Profile Figure 1-2 Architecture defined by CLI specifications
Briefly, the CLI defines two implementations: a minimal implementation known as a Kernel Profile and a more feature rich Compact Profile. The kernel contains the types and classes required by a compiler that is CLI compliant. The Base Class Library holds the basic data type classes, as well as classes that provide simple file access, define security attributes, and implement one-dimensional arrays. The Compact Profile adds three class libraries: an XML library that defines simple XML parsing, a Network library that provides HTTP support and access to ports, and a Reflection library that supports reflection (a way for a program to examine itself through metacode). This book, which describes the Microsoft implementation, would be considerably shorter if it described only the CLI recommendations. There would be no chapters on ADO.NET (database classes), ASP.NET (Web classes), or Windows Forms—and the XML chapters would be greatly reduced. As you may guess, these libraries depend on the underlying Windows API for functionality. In addition, .NET permits a program to invoke the Win32 API using an Interop feature. This means that a .NET developer has access not only to the Win32 API but also legacy applications and components (COM). By keeping this rather wide bridge to Windows, Microsoft’s .NET implementation becomes more of a transparent than virtual environment—not that there’s anything wrong with that. It gives developers making the transition to .NET the ability to create hybrid applications that combine .NET components with preexisting code. It also means that the .NET implementation code is not going to be ported to another operating system.
1.2 Common Language Runtime
The good news for developers—and readers of this book—is that the additional Microsoft features are being adopted by CLI open source initiatives. Mono2, one of the leading CLI projects, already includes major features such as ADO.NET, Windows Forms, full XML classes, and a rich set of Collections classes. This is particularly significant because it means the knowledge and skills obtained working with Microsoft .NET can be applied to implementations on Linux, BSD, and Solaris platforms. With that in mind, let’s take an overview of the Microsoft CLI implementation.
1.2
Common Language Runtime
The Common Language Runtime manages the entire life cycle of an application: it locates code, compiles it, loads associated classes, manages its execution, and ensures automatic memory management. Moreover, it supports cross-language integration to permit code generated by different languages to interact seamlessly. This section peers into the inner workings of the Common Language Runtime to see how it accomplishes this. It is not an in-depth discussion, but is intended to make you comfortable with the terminology, appreciate the language-neutral architecture, and understand what’s actually happening when you create and execute a program.
Pascal
C#
VB.NET
J#
C++
Perl
Base Class Libraries
IL + Metadata Common Language Runtime Class Loader
Execution Support
Just-in-Time Compiler
Security Memory Management
Native Code
CPU Figure 1-3
Common Language Runtime functions
2. http://www.mono-project.com/Main_Page
9
10
Chapter 1
■
Introduction to .NET and C#
Compiling .NET Code Compilers that are compliant with the CLR generate code that is targeted for the runtime, as opposed to a specific CPU. This code, known variously as Common Intermediate Language (CIL), Intermediate Language (IL), or Microsoft Intermediate Language (MSIL), is an assembler-type language that is packaged in an EXE or DLL file. Note that these are not standard executable files and require that the runtime’s Just-in-Time (JIT) compiler convert the IL in them to a machine-specific code when an application actually runs. Because the Common Language Runtime is responsible for managing this IL, the code is known as managed code. This intermediate code is one of the keys to meeting the .NET Framework’s formal objective of language compatibility. As Figure 1-3 illustrates, the Common Language Runtime neither knows—nor needs to know—which language an application is created in. Its interaction is with the language-independent IL. Because applications communicate through their IL, output from one compiler can be integrated with code produced by a different compiler. Another .NET goal, platform portability, is addressed by localizing the creation of machine code in the JIT compiler. This means that IL produced on one platform can be run on any other platform that has its own framework and a JIT compiler that emits its own machine code. In addition to producing IL, compilers that target the CLR must emit metadata into every code module. The metadata is a set of tables that allows each code module to be self-descriptive. The tables contain information about the assembly containing the code, as well as a full description of the code itself. This information includes what types are available, the name of each type, type members, the scope or visibility of the type, and any other type features. Metadata has many uses: •
•
•
The most important use is by the JIT compiler, which gathers all the type information it needs for compiling directly from the metacode. It also uses this information for code verification to ensure the program performs correct operations. For example, the JIT ensures that a method is called correctly by comparing the calling parameters with those defined in the method’s metadata. Metadata is used in the Garbage Collection process (memory management). The garbage collector (GC) uses metadata to know when fields within an object refer to other objects so that the GC can determine what objects can and can’t have their memory reclaimed. .NET provides a set of classes that provide the functionality to read metadata from within a program. This functionality is known collectively as reflection. It is a powerful feature that permits a program to query the code at runtime and make decisions based on its discovery. As we will see later in the book, it is the key to working with custom attributes, which are a C#-supported construct for adding custom metadata to a program.
1.2 Common Language Runtime
IL and metadata are crucial to providing language interoperability, but its real-world success hinges on all .NET compilers supporting a common set of data types and language specifications. For example, two languages cannot be compatible at the IL level if one language supports a 32-bit signed integer and the other does not. They may differ syntactically (for example, C# int versus a Visual Basic Integer), but there must be agreement of what base types each will support. As discussed earlier, the CLI defines a formal specification, called the Common Type System (CTS), which is an integral part of the Common Language Runtime. It describes how types are defined and how they must behave in order to be supported by the Common Language Runtime.
Common Type System The CTS provides a base set of data types for each language that runs on the .NET platform. In addition, it specifies how to declare and create custom types, and how to manage the lifetime of instances of these types. Figure 1-4 shows how .NET organizes the Common Type System.
Object
Class
Primitives
Interface
Structures
Array
Enums
Reference Types Figure 1-4
Value Types
Base types defined by Common Type System
Two things stand out in this figure. The most obvious is that types are categorized as reference or value types. This taxonomy is based on how the types are stored and accessed in memory: reference types are accessed in a special memory area (called a heap) via pointers, whereas value types are referenced directly in a program stack. The other thing to note is that all types, both custom and .NET defined, must inherit from the predefined System.Object type. This ensures that all types support a basic set of inherited methods and properties.
11
12
Chapter 1
■
Introduction to .NET and C#
Core Note In .NET, “type” is a generic term that refers to a class, structure, enumeration, delegate, or interface.
A compiler that is compliant with the CTS specifications is guaranteed that its types can be hosted by the Common Language Runtime. This alone does not guarantee that the language can communicate with other languages. There is a more restrictive set of specifications, appropriately called the Common Language Specification (CLS), that provides the ultimate rules for language interoperability. These specifications define the minimal features that a compiler must include in order to target the CLR. Table 1-1 contains some of the CLS rules to give you a flavor of the types of features that must be considered when creating CLS-compliant types (a complete list is included with the .NET SDK documentation). Table 1-1 Selected Common Language Specification Features and Rules Feature
Rule
Visibility (Scope)
The rules apply only to those members of a type that are available outside the defining assembly.
Characters and casing
For two variables to be considered distinct, they must differ by more than just their case.
Primitive types
The following primitive data types are CLS compliant: Byte, Int16, Int32, Int64, Single, Double, Boolean, Char, Decimal, IntPtr, and String.
Constructor invocation
A constructor must call the base class’s constructor before it can access any of its instance data.
Array bounds
All dimensions of arrays must have a lower bound of zero (0).
Enumerations
The underlying type of an enumeration (enum) must be of the type Byte, Int16, Int32, or Int64.
Method signature
All return and parameter types used in a type or member signature must be CLS compliant.
These rules are both straightforward and specific. Let’s look at a segment of C# code to see how they are applied:
1.2 Common Language Runtime
public class Conversion { public double Metric( double inches) { return (2.54 * inches); } public double metric( double miles) { return (miles / 0.62); } }
Even if you are unfamiliar with C# code, you should still be able to detect where the code fails to comply with the CLS rules. The second rule in the table dictates that different names must differ by more than case. Obviously, Metric fails to meet this rule. This code runs fine in C#, but a program written in Visual Basic.NET—which ignores case sensitivity—would be unable to distinguish between the upper and lowercase references.
Assemblies All of the managed code that runs in .NET must be contained in an assembly. Logically, the assembly is referenced as one EXE or DLL file. Physically, it may consist of a collection of one or more files that contain code or resources such as images or XML data. Assembly
Manifest Metadata
Name: FabricLib Other files: Public types: Type: Private Version Number: 1.1.3.04 Strong Name:
IL
FabricLib.dll
Figure 1-5
Single file assembly
An assembly is created when a .NET compatible compiler converts a file containing source code into a DLL or EXE file. As shown in Figure 1-5, an assembly contains a manifest, metadata, and the compiler-generated Intermediate Language (IL). Let’s take a closer look at these:
13
14
Chapter 1
■
Introduction to .NET and C#
Manifest. Each assembly must have one file that contains a manifest. The manifest is a set of tables containing metadata that lists the names of all files in the assembly, references to external assemblies, and information such as name and version that identify the assembly. Strongly named assemblies (discussed later) also include a unique digital signature. When an assembly is loaded, the CLR’s first order of business is to open the file containing the manifest so it can identify the members of the assembly. Metadata. In addition to the manifest tables just described, the C# compiler produces definition and reference tables. The definition tables provide a complete description of the types contained in the IL. For instance, there are tables defining types, methods, fields, parameters, and properties. The reference tables contain information on all references to types and other assemblies. The JIT compiler relies on these tables to convert the IL to native machine code. IL. The role of Intermediate Language has already been discussed. Before the CLR can use IL, it must be packaged in an EXE or DLL assembly. The two are not identical: an EXE assembly must have an entry point that makes it executable; a DLL, on the other hand, is designed to function as a code library holding type definitions. The assembly is more than just a logical way to package executable code. It forms the very heart of the .NET model for code deployment, version control, and security: •
•
•
All managed code, whether it is a stand-alone program, a control, or a DLL library containing reusable types, is packaged in an assembly. It is the most atomic unit that can be deployed on a system. When an application begins, only those assemblies required for initialization must be present. Other assemblies are loaded on demand. A judicious developer can take advantage of this to partition an application into assemblies based on their frequency of use. In .NET jargon, an assembly forms a version boundary. The version field in the manifest applies to all types and resources in the assembly. Thus, all the files comprising the assembly are treated as a single unit with the same version. By decoupling the physical package from the logical, .NET can share a logical attribute among several physical files. This is the fundamental characteristic that separates an assembly from a system based on the traditional DLLs. An assembly also forms a security boundary on which access permissions are based. C# uses access modifiers to control how types and type members in an assembly can be accessed. Two of these use the assembly as a boundary: public permits unrestricted access from any assembly; internal restricts access to types and members within the assembly.
1.2 Common Language Runtime
As mentioned, an assembly may contain multiple files. These files are not restricted to code modules, but may be resource files such as graphic images and text files. A common use of these files is to permit resources that enable an application to provide a screen interface tailored to the country or language of the user. There is no limit to the number of files in the assembly. Figure 1-6 illustrates the layout of a multi-file assembly. Assembly
Manifest
Metadata
Metadata
IL (MSIL) ApparelLib.dll
IL
FabricLib.dll Figure 1-6
Schematic.jpg Multi-file assembly
In the multi-file assembly diagram, notice that the assembly’s manifest contains the information that identifies all files in the assembly. Although most assemblies consist of a single file, there are several cases where multi-file assemblies are advantageous: •
•
•
They allow you to combine modules created in different programming languages. A programming shop may rely on Visual Basic.NET for its Rapid Application Development (RAD) and C# for component or enterprise development. Code from both can coexist and interact in the .NET assembly. Code modules can be partitioned to optimize how code is loaded into the CLR. Related and frequently used code should be placed in one module; infrequently used code in another. The CLR does not load the modules until they are needed. If creating a class library, go a step further and group components with common life cycle, version, and security needs into separate assemblies. Resource files can be placed in their own module separate from IL modules. This makes it easier for multiple applications to share common resources.
15
16
Chapter 1
■
Introduction to .NET and C#
Multi-file assemblies can be created by executing the C# compiler from the command line or using the Assembly Linker utility, Al.exe. An example using the C# compiler is provided in the last section of this chapter. Notably, Visual Studio.NET 2005 does not support the creation of multi-file assemblies.
Private and Shared Assemblies Assemblies may be deployed in two ways: privately or globally. Assemblies that are located in an application’s base directory or a subdirectory are called privately deployed assemblies. The installation and updating of a private assembly could not be simpler. It only requires copying the assembly into the directory, called the AppBase, where the application is located. No registry settings are needed. In addition, an application configuration file can be added to override settings in an application’s manifest and permit an assembly’s files to be moved within the AppBase. A shared assembly is one installed in a global location, called the Global Assembly Cache (GAC), where it is accessible by multiple applications. The most significant feature of the GAC is that it permits multiple versions of an assembly to execute side-by-side. To support this, .NET overcomes the name conflict problem that plagues DLLs by using four attributes to identify an assembly: the file name, a culture identity, a version number, and a public key token.
Figure 1-7
Partial listing of Global Assembly Directory
Public assemblies are usually located in the assembly directory located beneath the system directory of the operating system (WINNT\ on a Microsoft Windows 2000 operating system). As shown in Figure 1-7, the assemblies are listed in a special format that displays their four attributes (.NET Framework includes a DLL file that extends Windows Explorer to enable it to display the GAC contents). Let’s take a quick look at these four attributes: Assembly Name. Also referred to as the friendly name, this is the file name of the assembly minus the extension. Version. Every assembly has a version number that applies to all files in the assembly. It consists of four numbers in the format ...
1.2 Common Language Runtime
Typically, the major and minor version numbers are updated for changes that break backward compatibility. A version number can be assigned to an assembly by including an AssemblyVersion attribute in the assembly’s source code. Culture Setting. The contents of an assembly may be associated with a particular culture or language. This is designated by a two-letter code such as “en” for English or “fr” for French, and can be assigned with an AssemblyCulture attribute placed in source code: [assembly: AssemblyCulture ("fr-CA")]
Public Key Token. To ensure that a shared assembly is unique and authentic, .NET requires that the creator mark the assembly with a strong name. This process, known as signing, requires the use of a public/private key pair. When the compiler builds the assembly, it uses the private key to generate a strong name. The public key is so large that a token is created by hashing the public key and taking its last eight bytes. This token is placed in the manifest of any client assembly that references a shared assembly and is used to identify the assembly during execution.
Core Note An assembly that is signed with a public/private key is referred to as a strongly named assembly. All shared assemblies must have a strong name.
Precompiling an Assembly After an assembly is loaded, the IL must be compiled to the machine’s native code. If you are used to working with executables already in a machine code format, this should raise questions about performance and whether it’s possible to create equivalent “executables” in .NET. The answer to the second part of the statement is yes; .NET does provide a way to precompile an assembly. The .NET Framework includes a Native Image Generator (Ngen) tool that is used to compile an assembly into a “native image” that is stored in a native image cache— a reserved area of the GAC. Any time the CLR loads an assembly, it checks the cache to see if it has an associated native image available; if it does, it loads the precompiled code. On the surface, this seems a good idea to improve performance. However, in reality, there are several drawbacks. Ngen creates an image for a hypothetical machine architecture, so that it will run, for example, on any machine with an x86 processor. In contrast, when the JIT in .NET runs, it is aware of the specific machine it is compiling for and can accordingly
17
18
Chapter 1
■
Introduction to .NET and C#
make optimizations. The result is that its output often outperforms that of the precompiled assembly. Another drawback to using a native image is that changes to a system’s hardware configuration or operating system—such as a service pack update—often invalidate the precompiled assembly.
Core Recommendation As a rule, a dynamically compiled assembly provides performance equal to, or better than, that of a precompiled executable created using Ngen.
Code Verification As part of the JIT compile process, the Common Language Runtime performs two types of verification: IL verification and metadata validation. The purpose is to ensure that the code is verifiably type-safe. In practical terms, this means that parameters in a calling and called method are checked to ensure they are the same type, or that a method returns only the type specified in its return type declaration. In short, the CLR searches through the IL and metadata to make sure that any value assigned to a variable is of a compatible type; if not, an exception occurs.
Core Note By default, code produced by the C# compiler is verifiably type-safe. However, there is an unsafe keyword that can be used to relax memory access restrictions within a C# program (such as referencing beyond an array boundary).
A benefit of verified code is that the CLR can be certain that the code cannot affect another application by accessing memory outside of its allowable range. Consequently, the CLR is free to safely run multiple applications in a single process or address space, improving performance and reducing the use of OS resources.
1.3
Framework Class Library
The Framework Class Library (FCL) is a collection of classes and other types (enumerations, structures, and interfaces) that are available to managed code written in
1.3 Framework Class Library
any language that targets the CLR. This is significant, because it means that libraries are no longer tied to specific compilers. As a developer, you can familiarize yourself with the types in a library and be assured that you can use this knowledge with whatever .NET language you choose. The resources within the FCL are organized into logical groupings called namespaces. For the most part, these groupings are by broad functionality. For example, types used for graphical operations are grouped into the System.Drawing and System.Drawing.Drawing2D namespaces; types required for file I/O are members of the System.IO namespace. Namespaces represent a logical concept, not a physical one. The FCL comprises hundreds of assemblies (DLLs), and each assembly may contain multiple namespaces. In addition, a namespace may span multiple assemblies. To demonstrate, let’s look inside an FCL assembly.
Figure 1-8 Output from Ildasm shows the namespaces and types that comprise an assembly
Figure 1-8 displays a portion of the output generated by using Ildasm.exe to examine the contents of the mscorlib assembly. Although this only a partial listing, you can see that mscorlib contains System, the preeminent namespace in .NET, which serves as a repository for the types that give .NET its basic functionality. The assembly is also home to the System.Collections namespace, which includes classes and interfaces used for manipulating collections of data.
19
20
Chapter 1
■
Introduction to .NET and C#
Table 1-2 lists some of the most important namespaces in .NET. For reference, the last column in each row includes a chapter number in this book where you’ll find the namespace(s) used. Table 1-2 Selected FCL Namespaces Namespace
Use
Chapter
System
Contains the basic data types used by all applications. It also contains exception classes, predefined attributes, a Math library, and classes for managing the application environment.
3, 18
System.Collections System.Collections.Specialized System.Collections.Generic
Interfaces and classes used to man- 4 age collections of objects. These collections include the ArrayList, Hashtable, and Stack.
System.Data System.Data.OracleClient System.Data.SqlClient System.Data.OleDb System.Data.Odbc
Classes used for database operations (ADO.NET). The client namespaces support Oracle and SQL Server, respectively; OledDb and Odbc define the data connection used.
11, 12
System.Diagnostics
Contains classes that can be used to trace program execution, debug, and work with system logs and performance counters.
13
System.Drawing System.Drawing.Drawing2D System.Drawing.Printing System.Drawing.Text
Provides graphics functionality for GDI+. These namespaces contain a class used for drawing as well as pens, brushes, geometric shapes, and fonts.
8, 9
System.Globalization
Contains classes that define culture-related information that affects the way dates, currency, and symbols are represented.
5
System.IO
Provides file and data stream I/O. These classes provide a way to access the underlying file systems of the host operating system.
5
1.3 Framework Class Library
Table 1-2 Selected FCL Namespaces (continued) Namespace
Use
System.Net
Classes that support network proto- 17 cols and operations. Examples include WebRequest and WebResponse that request and fetch a Web page.
System.Reflection System.Reflection.Emit
7, 15, Contains types that permit the App. B runtime inspection of metadata. The Emit namespace allows a compiler or tool to generate metadata and IL dynamically.
System.Runtime.InterOpServices
Provides interoperability between managed and unmanaged code such as legacy DLLs or COM.
System.Security System.Security.Permissions System.Security.Cryptography
Classes used to manage .NET secu- 5, 15 rity. Defines classes that control access to operations and resources.
System.Text.RegularExpressions
Classes that support .NET’s regular expression engine.
5
System.Threading System.Threading.Thread
Manages threading activites: thread creation, synchronization, and thread pool access.
13
System.Web System.Web.Services System.Web.UI System.Web.UI.WebControls System.Web.Security
16, 17, 18 The Internet-related classes referred to as ASP.NET. They manage browser-server communication requirements, manipulate cookies, and contain the controls that adorn a Web page. Web.Services includes those classes required for SOAP-based XML messaging. Web.UI includes classes and interfaces used for creating controls and pages that comprise Web forms.
Chapter
8
21
22
Chapter 1
■
Introduction to .NET and C#
Table 1-2 Selected FCL Namespaces (continued) Namespace
Use
Chapter
System.Windows.Forms
Classes used to build Windows desktop GUI applications. Controls including the ListBox, TextBox, DataGrid, and buttons are found here.
6, 7
System.Xml
Types for processing XML.
10
Namespaces provide a roadmap for navigating the FCL. For example, if your applications are Web based, you’ll spend most of your time exploring the types in the System.Web.* namespaces. After you have learned the basics of .NET and gained proficiency with C#, you’ll find that much of your time is spent familiarizing yourself with the built-in types contained in the Framework Class Library.
1.4
Working with the .NET Framework and SDK
The .NET Framework Software Development Kit (SDK) contains the tools, compilers, and documentation required to create software that will run on any machine that has the .NET Framework installed. It is available as a free download (100 megabytes) from Microsoft that can be installed on Windows XP, Windows 2000, Windows Server 2003, and subsequent Windows operating systems. If you have Visual Studio.NET installed, there is no need to download it because VS.NET automatically does it for you. Clients using software developed with the SDK do not require the SDK on their machine; however, they do require a compatible version of the .NET Framework. This .NET Framework Redistributable is available as a free download3 (20+ megabytes) and should be distributed to clients along with applications that require it. This redistributable can be installed on Windows 98 and ME, in addition to the ones listed for the SDK. With minor exceptions, .NET applications will run identically on all operating system platforms, because they are targeted for the Common Language Runtime and not the operating system. There are some system requirements such as a minimum Internet Explorer version of 5.01. These are listed at the download site. 3. http://msdn.microsoft.com/netframework/downloads/updates/ default.aspx
1.4 Working with the .NET Framework and SDK
Updating the .NET Framework Unlike many development environments, installing a new version of the framework is almost effortless. The installation process places the updated version in a new directory having the name of the version. Most importantly, there is no file dependency between the new and older versions. Thus, all versions are functional on your system. Although it varies by operating system, the versions are usually in the path \winnt\Microsoft.NET\Framework\v1.0.3705 \winnt\Microsoft.NET\Framework\v1.1.4322 \winnt\Microsoft.NET\Framework\v2.0.40607
The installation of any new software version raises the question of compatibility with applications developed using an older version. .NET makes it easy to run existing applications against any framework version. The key to this is the application configuration file (discussed in much greater detail in Chapter 15). This text file contains XML tags and elements that give the CLR instructions for executing an application. It can specify where external assemblies are located, which version to use, and, in this case, which versions of the .NET Framework an application or component supports. The configuration file can be created with any text editor, although it’s preferable to rely on tools (such as the Framework Configuration tool) designed for the task. Your main use of the configuration file will be to test current applications against new framework releases. Although it can be done, it usually makes no sense to run an application against an earlier version than it was originally compiled against.
.NET Framework Tools The .NET Framework automates as many tasks as possible and usually hides the details from the developer. However, there are times when manual intervention is required. These may be a need to better understand the details of an assembly or perform the housekeeping required to prepare an application for deployment. We have encountered several examples of such tasks throughout the chapter. These include the need to • • • • •
Add a file to an assembly View the contents of an assembly View the details of a specific class Generate a public/private key pair in order to create a strongly named assembly Edit configuration files
Many of these are better discussed within the context of later chapters. However, it is useful to be aware of which tools are available for performing these tasks; and a
23
24
Chapter 1
■
Introduction to .NET and C#
few, such as those for exploring classes and assemblies, should be mastered early in the .NET learning curve. Table 1-3 lists some of the useful tools available to develop and distribute your applications. Three of these, Ildasm.exe, wincv.exe, and the .NET Framework Configuration tool, are the subject of further discussion. Table 1-3 Selected .NET Framework Tools Tool
Description
Al.exe
Can be used for creating an assembly composed of modules from different compilers. It is also used to build resource-only (satellite) assemblies.
Assembly Linker
Fuslogvw.exe
Assembly Binding Log Viewer Gacutil.exe
Global Assembly Cache tool Ildasm.exe
MSIL Disassembler Mscorcfg.msc
.NET Framework Configuration tool
Ngen.exe
Native Image Generator Sn.exe
Strong Name tool wincv.exe
Windows Forms Class Viewer
Used to troubleshoot the assembly loading process. It traces the steps followed while attempting to load an assembly. Is used to install or delete an assembly in the Global Assembly Cache. It can also be used for listing the GAC’s contents. A tool for exploring an assembly, its IL, and metadata. A Microsoft Management Console (MMC) snap-in used to configure an assembly while avoiding direct manual changes to an application’s configuration file. Designed primarily for administrators, a subset, Framework Wizards. Available for individual programmers. Compiles an assembly’s IL into native machine code. This image is then placed in the native image cache. Generates the keys that are used to create a strong—or signed—assembly. A visual interface to display searchable information about a class.
Generates descriptive information about a Web Web Services Description Language tool Service that is used by a client to access the service. Wsdl.exe
1.4 Working with the .NET Framework and SDK
Many of these tools are located in an SDK subdirectory: c:\Program Files\Microsoft.NET\SDK\v2.0\Bin
To execute the tools at the command line (on a Windows operating system) while in any directory, it is first necessary to place the path to the utilities in the system Path variable. To do this, follow these steps: 1. Right click on the My Computer icon and select Properties. 2. Select Advanced – Environment Variables. 3. Choose the Path variable and add the SDK subdirectory path to it. If you have Visual Studio installed, a simpler approach is to use the preconfigured Visual Studio command prompt. It automatically initializes the path information that enables you to access the command-line tools.
Ildasm.exe The Intermediate Language Disassembler utility is supplied with the .NET Framework SDK and is usually located in the Bin subdirectory along the path where the SDK is installed. It is invaluable for investigating the .NET assembly environment and is one of the first tools you should become familiar with as you begin to work with .NET assemblies and code. The easiest way to use the utility is to type in C:\>Ildasm /adv
at a command-line prompt (the optional /adv switch makes advanced viewing options available). This invokes the GUI that provides a File menu you use to select the assembly to view. Note that it does not open files in the Global Assembly Cache. Figure 1-9 shows an example of the output created when an assembly is opened in Ildasm. The contents are displayed in a readable, hierarchical format that contains the assembly name, corecsharp1, and all of its members. This hierarchy can then be used to drill down to the underlying IL (or CIL) instructions for a specific member. As an example, let’s consider the Conversion class. The figure shows that it consists of three methods: Metric, conversion, and metric. The original source code confirms this: public class Conversion { public double Metric( double inches) { return (2.54 * inches); } [CLSCompliantAttribute(false)] public double metric( double miles)
25
26
Chapter 1
■
Introduction to .NET and C#
{ return (miles / 0.62); } public double conversion( double pounds) { return (pounds * 454);} }
Figure 1-9
View assembly contents with Ildasm.exe
Double clicking on the Metric method brings up a screen that displays its IL (Figure 1-10).
Figure 1-10
View of the IL
1.4 Working with the .NET Framework and SDK
Ildasm can be used as a learning tool to solidify the concepts of IL and assemblies. It also has some practical uses. Suppose you have a third-party component (assembly) to work with for which there is no documentation. Ildasm provides a useful starting point in trying to uncover the interface details of the assembly.
Core Suggestion Ildasm has a File – Dump menu option that makes it useful for saving program documentation in a text file. Select Dump Metainfo to create a lengthy human-readable form of the assembly’s metadata; select Dump Statistics to view a profile of the assembly that details how many bytes each part uses.
Ildasm and Obfuscation One of the natural concerns facing .NET developers is how to protect their code when a tool such as Ildasm—and other commercially available disassemblers—can be used to expose it. One solution is to use obfuscation—a technique that uses renaming and code manipulation sleight of hand to make the contents of an assembly unreadable by humans. It is important to understand that obfuscation is not encryption. Encryption requires a decryption step so that the JIT compiler can process the code. Obfuscation transforms the IL code into a form that can be compiled using the tools of your development environment (see Figure 1-11).
Source Code
Compile
MSIL
CLR
Obfuscate
Obfuscated MSIL
CLR
Figure 1-11 Obfuscation conceals the original Intermediate Language
The obfuscated code is functionally equivalent to the assembly’s IL code and produces identical results when run by the CLR. How does it do this? The most common trick is to rename meaningful types and members with names that have no intrinsic meaning. If you look at obfuscated code, you’ll see a lot of types named “a”
27
28
Chapter 1
■
Introduction to .NET and C#
or “b,” for example. Of course, the obfuscation algorithm must be smart enough not to rename types that are used by outside assemblies that depend on the original name. Another common trick is to alter the control flow of the code without changing the logic. For example, a while statement may be replaced with a combination of goto and if statements. An obfuscator is not included in the .NET SDK. Dotfuscator Community Edition, a limited-feature version of a commercial product, is available with Visual Studio.NET. Despite being a relatively unsophisticated product—and only available for the Microsoft environment—it is a good way to become familiar with the process. Several vendors now offer more advanced obfuscator products.
wincv.exe WinCV is a class viewer analogous to the Visual Studio Object Viewer, for those not using Visual Studio. It is located in the Program Files\Microsoft.Net\ SDK\V1.x\Bin directory and can be run from the command prompt. When the window appears, type the name of the class you want to view into the Searching For box (see Figure 1-12).
Figure 1-12
Using WinCV to view type definition of the Array class
WinCV provides a wealth of information about any type in the base class libraries. The four highlighted areas provide a sampling of what is available:
1.4 Working with the .NET Framework and SDK
1. System.Array is the class that is being explored. 2. This class is located in the mscorlib.dll assembly. We have already mentioned that this assembly contains the .NET managed types. 3. This list contains the class, object, and interfaces that the Array class inherits from. 4. The definition of each method in the class is included. This definition, which includes accessibility, type, and parameters, is called the method’s signature.
Framework Configuration Tool This tool provides an easy way to manage and configure assemblies as well as set security policies for accessing code. This tool is packaged as a Microsoft Management Console (MMC) snap-in. To access it, select Administrative Tools from the Control Panel; then select the Microsoft .NET Framework Configuration tool. This tool is designed for administrators who need to do the following: • •
•
•
Manage assemblies. Assemblies can be added to the GAC or deleted. Configure assemblies. When an assembly is updated, the publisher of the assembly is responsible for updating the binding policy of the assembly. This policy tells the CLR which version of an assembly to load when an application references an assembly. For example, if assembly version 1.1 replaces 1.0, the policy redirects version 1.0 to 1.1 so that it is loaded. This redirection information is contained in a configuration file. View .NET Framework security and modify an assembly’s security. .NET security allows an assembly to be assigned certain permissions or rights. In addition, an assembly can require that other assemblies accessing it have certain permissions. Manage how individual applications interact with an assembly or set of assemblies. You can view a list of all assemblies an application uses and set the version that your application uses.
To illustrate a practical use of the configuration tool, let’s look at how it can be used to address one of the most common problems that plagues the software development process: the need to drop back to a previous working version when a current application breaks. This can be a difficult task when server DLLs or assemblies are involved. .NET offers a rather clever solution to this problem: Each time an application runs, it logs the set of assemblies that are used by the program. If they are unchanged from the previous run, the CLR ignores them; if there are changes, however, a snapshot of the new set of assemblies is stored.
29
30
Chapter 1
■
Introduction to .NET and C#
When an application fails, one option for the programmer is to revert to a previous version that ran successfully. The configuration tool can be used to redirect the application to an earlier assembly. However, there may be multiple assemblies involved. This is where the configuration tool comes in handy. It allows you to view previous assembly configurations and select the assemblies en masse to be used with the application. To view and select previous configurations, select Applications – Fix an Application from the Configuration tool menu. Figure 1-13 combines the two dialog boxes that subsequently appear. The main window lists applications that have run and been recorded. The smaller window (a portion of a larger dialog) is displayed when you click on an application. This window lists the most recent (up to five) configurations associated with the application. You simply select the assembly configuration that you want the application to use.
Figure 1-13 Using application configuration tool to select assembly version
This configuration tool is clearly targeted for administrators. Individual developers should rely on a subset of this tool that is packaged as three wizards: Adjust .NET Security, Trust An Assembly, and Fix An Application. Access these by selecting Framework Wizards from Administrative Tools.
1.5 Understanding the C# Compiler
1.5
Understanding the C# Compiler
Many developers writing nontrivial .NET applications rely on Visual Studio or some other Integrated Development Environment (IDE) to enter source code, link external assemblies, perform debugging, and create the final compiled output. If you fall into this category, it is not essential that you understand how to use the .NET SDK and raw C# compiler; however, it will increase your understanding of the .NET compilation process and give you a better feel for working with assemblies. As a byproduct, it will also acquaint you with the command line as a way to work with SDK programs. Many of the utilities presented in the previous section are invoked from the command line, and you will occasionally find it useful to perform compilation in that environment rather than firing up your IDE.
Visual Studio IL + Metacode Third-Party IDE SharpDevelop
SRC1 CSC.EXE
APP.EXE
SRC2 Text Editor
.cs LIB.DLL Figure 1-14
External Assembly
Compilation process
Figure 1-14 shows the basic steps that occur in converting source code to the final compiled output. The purpose of this section is to demonstrate how a text editor and the C# compiler can be used to build an application. Along the way, it will provide a detailed look at the many compiler options that are hidden by the IDE.
Locating the Compiler The C# compiler, csc.exe, is located in the path where the .NET Framework is installed: C:\winnt\Microsoft.NET\Framework\v2.0.40607
Of course, this may vary depending on your operating system and the version of Framework installed. To make the compiler available from the command line in any
31
32
Chapter 1
■
Introduction to .NET and C#
current directory, you must add this path to the system Path variable. Follow the steps described in the previous section for setting the path for the SDK utilities. Type in the following statement at the command line to verify that the compiler can be accessed: C:\>csc /help
Compiling from the Command Line To compile the C# console application client.cs into the executable client.exe, enter either of the following statements at the command prompt: C:\> csc client.cs C:\> csc /t:exe client.cs
Both statements compile the source into an executable (.exe) file—the default output from the compiler. As shown in Table 1-4, the output type is specified using the /t: flag. To create a DLL file, set the target value to library. For a WinForms application, specify /t:winexe. Note that you can use /t:exe to create a WinForms application, but the console will be visible as background window. Table 1-4 Selected Options for the C# Command-Line Compiler Option
Description
/addmodule
Specifies a module that is to be included in the assembly created. This is an easy way to create a multi-file assembly.
/debug
Causes debug information to be produced.
/define
Preprocessor directive can be passed to compiler: /define:DEBUG.
/delaysign
Builds an assembly using delayed signing of the strong name. This is discussed in Chapter 15.
/doc
Used to specify that an output file containing XML documentation is to be produced.
/keyfile
Specifies the path to the .snk file containing the key pair used for strong signing (see Chapter 15).
/lib
Specifies where assemblies included in the /reference option are located.
/out
Name of the file containing compiled output. The default is the name of the input file with .exe suffix.
1.5 Understanding the C# Compiler
Table 1-4 Selected Options for the C# Command-Line Compiler (continued) Option
Description
/reference (/r)
References an external assembly.
/resource
Used to embed resource files into the assembly that is created.
/target (/t)
Specifies the type of output file created: /t:exe builds a *.exe console application. This is the default
output. /t:library builds a *.dll assembly. /t:module builds a module (Portable Executable file) that
does not contain a manifest. /t:winexe builds a *.exe Windows Forms assembly.
The real value of working with the raw compiler is the ability to work with multiple files and assemblies. For demonstration purposes, create two simple C# source files: client.cs and clientlib.cs. client.cs using System; public class MyApp { static void Main(string[] args) { ShowName.ShowMe("Core C#"); } } clientlib.cs using System; public class ShowName { public static void ShowMe(string MyName) { Console.WriteLine(MyName); } }
It’s not important to understand the code details, only that the client routine calls a function in clientlib that writes a message to the console. Using the C# compiler, we can implement this relationship in a number of ways that not only demonstrate compiler options but also shed light on the use of assemblies.
33
34
Chapter 1
■
Introduction to .NET and C#
Example 1: Compiling Multiple Files The C# compiler accepts any number of input source files. It combines their output into a single file assembly: csc /out:client.exe client.cs clientlib.cs
Example 2: Creating and Using a Code Library The code in clientlib can be placed in a separate library that can be accessed by any client: csc /t:library clientlib.cs
The output is an assembly named clientlib.dll. Now, compile the client code and reference this external assembly: csc /r:clientlib.dll
client.cs
The output is an assembly named client.exe. If you examine this with Ildasm, you see that the manifest contains a reference to the clientlib assembly.
Example 3: Creating an Assembly with Multiple Files Rather than existing as a separate assembly, clientlib can also be packaged as a separate file inside the client.exe assembly. Because only one file in an assembly may contain a manifest, it is first necessary to complile clientlib.cs into a Portable Executable4 (PE) module. This is done by selecting module as the target output: csc /t:module clientlib.cs
The output file is clientfile.netmodule. Now, it can be placed in the client.exe assembly by using the compiler’s addmodule switch: csc /addmodule:clientlib.netmodule client.cs
The resultant assembly consists of two files: client.exe and clientlib.netmodule.
These examples, shown in Figure 1-15, illustrate the fact that even a simple application presents the developer with multiple architectural choices for implementing an application.
4. The PE format defines the layout for executable files that run on 32- or 64-bit Windows systems.
1.6 Summary
Example 1: Example 2: Multiple Source Reference External Files Assembly
Example 3: Multi-File Assembly
Manifest
Manifest
Manifest
Manifest
Metadata
Metadata
Metadata
Metadata
IL
IL
IL
IL
client.exe
client.exe
clientlib.dll
client.exe
Metadata IL clientlib. netmodule
Figure 1-15 Options for deploying an application
1.6
Summary
The .NET Framework consists of the Common Language Runtime (CLR) and the Framework Class Library (FCL). The CLR manages all the tasks associated with code execution. It first ensures that code is CLR compliant based on the Common Language Specification (CLS) standard. It then loads an application and locates all dependent assemblies. Its Just-in-Time (JIT) compiler converts the IL contained in an application’s assembly, the smallest deployable code unit in .NET, into native machine code. During the actual program execution, the CLR handles security, manages threads, allocates memory, and performs garbage collection for releasing unused memory. All code must be packaged in an assembly in order for the CLR to use it. An assembly is either a single file or grouping of multiple physical files treated as a single unit. It may contain code modules as well as resource files. The FCL provides a reusable set of classes and other types that are available to all CLR-compliant code. This eliminates the need for compiler-specific libraries. Although the FCL consists of several physical DLLs containing over a thousand types, it’s made manageable by the use of namespaces that impose a logical hierarchy over all the types. To assist the developer in debugging and deploying software, .NET includes a set of utilities that enables an administrator to perform such tasks as managing assemblies, precompiling assemblies, adding files to an assembly, and viewing class details. In addition, a wealth of open source .NET tools is becoming available to aid the development process.
35
36
Chapter 1
1.7
■
Introduction to .NET and C#
Test Your Understanding
1. What portable environment must be installed on a client’s machine to enable it to run a .NET application? 2. What is managed code? What is unmanaged code? 3. What is the difference between the Common Type System and the Common Language Specification? 4. How does the CLR allow code from different compilers to interact? 5. What is the role of the Global Assembly Cache? 6. What four components make up the identity of a strongly named assembly? 7. What is the relationship between a namespace and an assembly? 8. Describe what these commonly used acronyms stand for: CLR, GAC, FCL, IL.
This page intentionally left blank
C# LANGUAGE FUNDAMENTALS
Topics in This Chapter • Overview of a C# Program: In addition to the basic elements that comprise a C# program, a developer needs to be aware of other .NET features such as commenting options and recommended naming conventions. • Primitives: Primitives are the basic data types defined by the FCL to represent numbers, characters, and dates. • Operators: C# uses traditional operator syntax to perform arithmetic and conditional operations. • Program Flow Statements: Program flow can be controlled using if and switch statements for selection; and while, do, for, and foreach clauses for iteration. • String: The string class supports the expected string operations: concatenation, extracting substrings, searching for instances of a character pattern, and both case sensitive and insensitive comparisons. • Enums: An enumeration is a convenient way to assign descriptions that can be used to reference an underlying set of values. • Using Arrays: Single- or multi-dimensional arrays of any type can be created in C#. After an array is created, the System.Array class can be used to sort and copy the array. • Reference and Value Types: All types in .NET are either a value or reference type. It is important to understand the differences and how they can affect a program’s performance.
2
In September 2000, an ECMA1 (international standardization group for information and communication systems) task group was established to define a Microsoft proposed standard for the C# programming language. Its stated design goal was to produce “a simple, modern, general-purpose, object-oriented programming language.” The result, defined in a standard known as ECMA-334, is a satisfyingly clean language with a syntax that resembles Java, and clearly borrows from C++ and C. It’s a language designed to promote software robustness with array bounds checking, strong type checking, and the prohibition of uninitialized variables. This chapter introduces you to the fundamentals of the language: It illustrates the basic parts of a C# program; compares value and reference types; and describes the syntax for operators and statements used for looping and controlling program flow. As an experienced programmer, this should be familiar terrain through which you can move quickly. However, the section on value and reference types may demand a bit more attention. Understanding the differences in how .NET handles value and reference types can influence program design choices.
1. ECMA International was formerly known as European Computer Manufacturers Association and is referred to herein simply as ECMA.
39
40
Chapter 2
2.1
■
C# Language Fundamentals
The Layout of a C# Program
Figure 2-1 illustrates some of the basic features of a C# program.
using Class
XML Comment
Main()
// (1) using simplifies references to namespaces using System; // (2) A Class Declaration class Apparel { /// public double Price = 250.0; /// public string FabType = "Synthetic"; } /// Entry point to program public class MyApp { // (3) Main() is required in each C# program static void Main() { Apparel myApparel = new Apparel(); string myType = myApparel.FabType; Console.WriteLine(myApparel.Price, myType); } }
Figure 2-1
Basic elements of a C# program
The code in Figure 2-1 consists of a class MyApp that contains the program logic and a class Apparel that contains the data. The program creates an instance of Apparel and assigns it to myApparel. This object is then used to print the values of the class members FabType and Price to the console. The important features to note include the following: 1. The using statement specifies the namespace System. Recall from Chapter 1, “Introduction to .NET and C#,” that the .NET class libraries are organized into namespaces and that the System namespace contains all of the simple data types. The using statement tells the compiler to search this namespace when resolving references, making it unnecessary to use fully qualified names. For example, you can refer to label rather than System.Web.UI.WebControls.Label. 2. All programming logic and data must be contained within a type definition. All program logic and data must be embedded in a class, structure, enum, interface, or delegate. Unlike Visual Basic, for
2.1 The Layout of a C# Program
instance, C# has no global variable that exists outside the scope of a type. Access to types and type members is strictly controlled by access modifiers. In this example, the access modifier public permits external classes—such as MyApp—to access the two members of the Apparel class. 3. A Main() method is required for every executable C# application. This method serves as the entry point to the application; it must always have the static modifier and the M must be capitalized. Overloaded forms of Main()define a return type and accept a parameter list as input. Return an integer value: static int Main() { return 0; // must return an integer value }
Receive a list of command-line arguments as a parameter and return an integer value: static int Main(string[] args) { // loop through arguments foreach(string myArg in args) Console.WriteLine(myArg); return 0; }
The parameter is a string array containing the contents of the command line used to invoke the program. For example, this command line executes the program MyApparel and passes it two parameter values: C:\> MyApparel 5 6
Core Note The contents of the command line are passed as an argument to the Main() method. The System.Environment.CommandLine property also exposes the command line’s contents.
41
42
Chapter 2
■
C# Language Fundamentals
General C# Programming Notes Case Sensitivity All variable and keywords are distinguished by case sensitivity. Replace class with Class in Figure 2-1 and the code will not compile.
Naming Conventions The ECMA standard provides naming convention guidelines to be followed in your C# code. In addition to promoting consistency, following a strict naming policy can minimize errors related to case sensitivity that often result from undisciplined naming schemes. Table 2-1 summarizes some of the more important recommendations. Note that the case of a name may be based on two capitalization schemes: 1. Pascal. The first character of each word is capitalized (for example, MyClassAdder). 2. Camel. The first character of each word, except the first, is capitalized (for example, myClassAdder). Table 2-1 C# Naming Conventions Type
Case
Notes and Examples
Class
Pascal
• Use noun or noun phrases. • Try to avoid starting with I because this is reserved for interfaces. • Do not use underscores.
Constant
Pascal
public const double GramToPound = 454.0 ;
Enum Type
Pascal
• Use Pascal case for the enum value names. • Use singular name for enums. public enum WarmColor { Orange, Yellow, Brown}
Event
Pascal
• The method that handles events should have the suffix EventHandler. • Event argument classes should have the suffix EventArgs.
Exception
Pascal
• Has suffix Exception.
Interface
Pascal
• Has prefix of I. IDisposable
Local Variable
Camel
• Variables with public access modifier use Pascal. int myIndex
2.1 The Layout of a C# Program
Table 2-1 C# Naming Conventions (continued) Type
Case
Notes and Examples
Method
Pascal
• Use verb or verb phrases for name.
Namespace
Pascal
• Do not have a namespace and class with the same name. • Use prefixes to avoid namespaces having the same name. For example, use a company name to categorize namespaces developed by that company. Acme.GraphicsLib
Property
Pascal
• Use noun or noun phrase.
Parameter
Camel
• Use meaningful names that describe the parameter’s purpose.
The rule of thumb is to use Pascal capitalization everywhere except with parameters and local variables.
Commenting a C# Program The C# compiler supports three types of embedded comments: an XML version and the two single-line (//) and multi-line (/* */) comments familiar to most programmers: // /*
for a single line for one or more lines */ /// XML comment describing a class
An XML comment begins with three slashes (///) and usually contains XML tags that document a particular aspect of the code such as a structure, a class, or class member. The C# parser can expand the XML tags to provide additional information and export them to an external file for further processing. The tag—shown in Figure 2-1—is used to describe a type (class). The C# compiler recognizes eight other primary tags that are associated with a particular program element (see Table 2-2). These tags are placed directly above the lines of code they refer to.
43
44
Chapter 2
■
C# Language Fundamentals
Table 2-2 XML Documentation Tags Tag
Description
Text illustrating an example of using a particular program feature goes between the beginning and ending tags.
cref attribute contains name of exception. ///
file attribute is set to name of another XML file that is to be included in the XML documentation produced by this source code.
name attribute contains the name of the parameter.
Most of the time this is set to the following: ///
Provides additional information about a type not found in the section.
Place a textual description of what is returned from a method or property between the beginning and ending tags.
The cref attribute is set to the name of an associated type, field, method, or other type member.
Contains a class description; is used by IntelliSense in VisualStudio.NET.
The value of the XML comments lies in the fact that they can be exported to a separate XML file and then processed using standard XML parsing techniques. You must instruct the compiler to generate this file because it is not done by default. The following line compiles the source code consoleapp.cs and creates an XML file consoleXML: C:\> csc consoleapp.cs /doc:consoleXML.xml
If you compile the code in Figure 2-1, you’ll find that the compiler generates warnings for all public members in your code: Warning CS1591: Missing XML comment for publicly visible type ...
2.2 Primitives
To suppress this, add the /nowarn:1591 option to the compile-line command. The option accepts multiple warning codes separated with a comma.
Core Note Many documentation tools are available to transform and extend the C# XML documentation output. One of the most advanced is NDoc (ndoc.sourceforge.net), an open source tool that not only formats the XML but uses reflection to glean further information about an assembly.
2.2
Primitives
The next three sections of this chapter describe features that you’ll find in most programming languages: variables and data types, operators, expressions, and statements that control the flow of operations. The discussion begins with primitives. As the name implies, these are the core C# data types used as building blocks for more complex class and structure types. Variables of this type contain a single value and always have the same predefined size. Table 2-3 provides a formal list of primitives, their corresponding core data types, and their sizes. Table 2-3 C# Primitive Data Types C# Primitive Type
FCL Data Type
Description
object
System.Object
Ultimate base type of all other types.
string
System.String
A sequence of Unicode characters.
decimal
System.Decimal
Precise decimal with 28 significant digits.
bool
System.Boolean
A value represented as true or false.
char
System.Char
A 16-bit Unicode character.
byte
System.Byte
8-bit unsigned integral type.
sbyte
System.SByte
8-bit signed integral type.
short
System.Int16
16-bit signed integral type.
int
System.Int32
32-bit signed integral type.
45
46
Chapter 2
■
C# Language Fundamentals
Table 2-3 C# Primitive Data Types (continued) C# Primitive Type
FCL Data Type
Description
long
System.Int64
64-bit signed integral type.
ushort
System.UInt16
16-bit unsigned integral type.
uint
System.UInt32
32-bit unsigned integral type.
ulong
System.UIint64
64-bit unsigned integral type.
single (float)
System.Single
Single-precision floating-point type.
double
System.Double
Double-precision floating-point type.
As the table shows, primitives map directly to types in the base class library and can be used interchangeably. Consider these statements: System.Int32 age = new System.Int32(17); int age = 17; System.Int32 age = 17;
They all generate exactly the same Intermediate Language (IL) code. The shorter version relies on C# providing the keyword int as an alias for the System.Int32 type. C# performs aliasing for all primitives. Here are a few points to keep in mind when working with primitives: •
The keywords that identify the value type primitives (such as int) are actually aliases for an underlying structure (struct type in C#). Special members of these structures can be used to manipulate the primitives. For example, the Int32 structure has a field that returns the largest 32-bit integer and a method that converts a numeric string to an integer value: int iMax = int.MaxValue; // Return largest integer int pVal = int.Parse("100"); // converts string to int
The C# compiler supports implicit conversions if the conversion is a “safe” conversion that results in no loss of data. This occurs when the target of the conversion has a greater precision than the object being converted, and is called a widening conversion. In the case of a narrowing conversion, where the target has less precision, the conversion must have explicit casting. Casting is used to coerce, or convert, a value of one type into that of another. This is done
2.2 Primitives
syntactically by placing the target data type in parentheses in front of the value being converted: int i = (int)y;. short i16 = 50; int i32 = i16; i16 = i32; i16 = (short) i32;
•
// // // //
16-bit integer Okay: int has greater precision Fails: short is 16 bit, int is 32 Okay since casting used
Literal values assigned to the types float, double, and decimal require that their value include a trailing letter: float requires F or f; double has an optional D or d; and decimal requires M or m. decimal pct = .15M; // M is required for literal value
The remainder of this section offers an overview of the most useful primitives with the exception of string, which is discussed later in the chapter.
decimal The decimal type is a 128-bit high-precision floating-point number. It provides 28 decimal digits of precision and is used in financial calculations where rounding cannot be tolerated. This example illustrates three of the many methods available to decimal type. Also observe that when assigning a literal value to a decimal type, the M suffix must be used. decimal iRate = decimal decimal decimal // Next decimal
iRate = 3.9834M; // decimal requires M decimal.Round(iRate,2); // Returns 3.98 dividend = 512.0M; divisor = 51.0M; p = decimal.Parse("100.05"); statement returns remainder = 2 rem = decimal.Remainder(dividend,divisor);
bool The only possible values of a bool type are true and false. It is not possible to cast a bool value to an integer—for example, convert true to a 1, or to cast a 1 or 0 to a bool. bool bt = true; string bStr = bt.ToString(); // returns "true" bt = (bool) 1; // fails
47
48
Chapter 2
■
C# Language Fundamentals
char The char type represents a 16-bit Unicode character and is implemented as an unsigned integer. A char type accepts a variety of assignments: a character value placed between individual quote marks (' '); a casted numeric value; or an escape sequence. As the example illustrates, char also has a number of useful methods provided by the System.Char structure: myChar = 'B'; // 'B' has an ASCII value of 66 myChar = (char) 66; // Equivalent to 'B' myChar = '\u0042'; // Unicode escape sequence myChar = '\x0042'; // Hex escape sequence myChar = '\t'; // Simple esc sequence:horizontal tab bool bt; string pattern = "123abcd?"; myChar = pattern[0]; // '1' bt = char.IsLetter(pattern,3); // true ('a') bt = char.IsNumber(pattern,3); // false bt = char.IsLower(pattern,0); // false ('1') bt = char.IsPunctuation(pattern,7); // true ('?') bt = char.IsLetterOrDigit(pattern,1); // true bt = char.IsNumber(pattern,2); // true ('3') string kstr="K"; char k = char.Parse(kstr);
byte, sbyte A byte is an 8-bit unsigned integer with a value from 0 to 255. An sbyte is an 8-bit signed integer with a value from –128 to 127. byte[] b = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF}; string s = b[4].ToString(); // returns 170 char myChar = (char) b[3];
short, int, long These represent 16-, 32-, and 64-bit signed integer values, respectively. The unsigned versions are also available (ushort, uint, ulong). short i16 = 200; i16 = 0xC8 ; // hex value for 200 int i32 = i16; // no casting required
2.2 Primitives
single, double These are represented in 32-bit single-precision and 64-bit double-precision formats. In .NET 1.x, single is referred to as float. • • •
•
The single type has a value range of 1.5 × 10 –45 to 3.4 × 1038 with 7-decimal digit precision. The double type has a value range of 5 × 10–324 to 1.7 × 10308 with 15- to 16-decimal digit precision. Floating-point operations return NaN (Not a Number) to signal that the result of the operation is undefined. For example, dividing 0.0 by 0.0 results in NaN. Use the System.Convert method when converting floating-point numbers to another type.
float xFloat = 24567.66F; int xInt = Convert.ToInt32(xFloat); // returns 24567 int xInt2 = (int) xFloat; if(xInt == xInt2) { } // False string xStr = Convert.ToString(xFloat); single zero = 0; if (Single.IsNaN(0 / zero)) { } // True double xDouble = 124.56D;
Note that the F suffix is used when assigning a literal value to a single type, and D is optional for a double type.
Using Parse and TryParse to Convert a Numeric String The primitive numeric types include Parse and TryParse methods that are used to convert a string of numbers to the specified numeric type. This code illustrates: short shParse int iParse long lparse decimal dParse float sParse double dbParse
= = = = = =
Int16.Parse("100"); Int32.Parse("100"); Int64.Parse("100"); decimal.Parse("99.99"); float.Parse("99.99"); double.Parse("99.99");
TryParse, introduced in .NET 2.0, provides conditional parsing. It returns a boolean value indicating whether the parse is successful, which provides a way to
49
50
Chapter 2
■
C# Language Fundamentals
avoid formal exception handling code. The following example uses an Int32 type to demonstrate the two forms of TryParse: int result; // parse string and place result in result parameter bool ok = Int32.TryParse("100", out result); bool ok = Int32.TryParse("100", NumberStyles.Integer, null, out result);
In the second form of this method, the first parameter is the text string being parsed, and the second parameter is a NumberStyles enumeration that describes what the input string may contain. The value is returned in the fourth parameter.
2.3
Operators: Arithmetic, Logical, and Conditional
The C# operators used for arithmetic operations, bit manipulation, and conditional program flow should be familiar to all programmers. This section presents an overview of these operators that is meant to serve as a syntactical reference.
Arithmetic Operators Table 2-4 summarizes the basic numerical operators. The precedence in which these operators are applied during the evaluation of an expression is shown in parentheses, with 1 being the highest precedence. Table 2-4 Numerical Operators Operator
Description
Example
+ -
(3)
Addition Subtraction
int x = y + 10;
* / %
(2)
Multiplication Division, Modulo
int int int y =
++ --
(1)
Prefix/postfix Increment/decrement
x = 5; Console.WriteLine(x++) // x = 5 Console.WriteLine(++x) // x = 6
~
(1)
Bitwise complement
int x = ~127;
x y z x
= = = %
60; 15; x * y / 2; // 450 29 ; // remainder is 2
// returns -128
2.3 Operators: Arithmetic, Logical, and Conditional
Table 2-4 Numerical Operators (continued) Operator (4)
>> 2; // 5 = 00101 Works with byte, char, short, int, and long
(5-6-7) Bitwise AND Bitwise OR Bitwise XOR
& | ^
byte x = 12; // 001100 byte y = 11; // 001011 int result = x & y; //8 = 001000 result = x ^ y; //7 = 000111
Core Note C# does not provide an exponentiation operator. Instead, use the Math.Pow() method to raise a number to a power, and Math.Exp() to raise e to a power.
Conditional and Relational Operators Relational operators are used to compare two values and determine their relationship. They are generally used in conjunction with conditional operators to form more complex decision constructs. Table 2-5 provides a summary of C# relational and conditional operators. Table 2-5 Relational and Conditional Boolean Operators Statement
Description
Example
== !=
Equality Inequality
if (x == y) {...}
=
Numeric less than Less than or equal to Greater than Greater than or equal to
if (x = 70) pass="pass"; else pass="fail"; // expression ? op1 : op2 pass = (grade >= 70) ? "pass" : "fail";
If the expression is true, the ?: operator returns the first value; if it’s false, the second is returned.
Control Flow Statements The C# language provides if and switch conditional constructs that should be quite familiar to C++ and Java programmers. Table 2-6 provides a summary of these statements. Table 2-6 Control Flow Statements Conditional Statement
Example
if (boolean expression) { // statements } else { // statements }
if (bmi < 24.9) { weight = "normal"; riskFactor = 2; } else { weight = "over"; riskFactor=6; }
2.3 Operators: Arithmetic, Logical, and Conditional
Table 2-6 Control Flow Statements (continued) Conditional Statement
Example
switch (expression) { case constant expression: // statements; // break/goto/return() case constant expression: // statements; // break/goto/return() default: // statements; // break/goto/return() }
switch (ndx) { case 1: fabric = "cotton"; blend = "100%"; break; case 2: // combine 2 & 3 case 3: fabric = "cotton"; blend = "60%"; break; default: // optional fabric = "cotton"; blend = "50%"; break; }
• Constant expression may be an integer, enum value, or string. • No “fall through” is permitted. Each case block must end with a statement that transfers control.
if-else Syntax: if ( boolean expression ) statement if ( boolean expression ) statement1 else statement2
C# if statements behave as they do in other languages. The only issue you may encounter is how to format the statements when nesting multiple if-else clauses. // Nested if statements if (age > 16) { if (sex == "M") { type = "Man"; } else { type = "Woman" ; } } else { type = "child"; }
if (age > 16) if (sex == "M") type = "Man"; else type = "Woman" ; else type = "child";
53
54
Chapter 2
■
C# Language Fundamentals
Both code segments are equivalent. The right-hand form takes advantage of the fact that curly braces are not required to surround single statements; and the subordinate if clause is regarded as a single statement, despite the fact that it takes several lines. The actual coding style selected is not as important as agreeing on a single style to be used.
switch Syntax: switch( expression ) {switch block}
The expression is one of the int types, a character, or a string. The switch block consists of case labels—and an optional default label—associated with a constant expression that must implicitly convert to the same type as the expression. Here is an example using a string expression: // switch with string expression using System; public class MyApp { static void Main(String[] args) { switch (args[0]) { case "COTTON": // is case sensitive case "cotton": Console.WriteLine("A good natural fiber."); goto case "natural"; case "polyester": Console.WriteLine("A no-iron synthetic fiber."); break; case "natural": Console.WriteLine("A Natural Fiber. "); break; default: Console.WriteLine("Fiber is unknown."); break; } } }
2.4 Loops
The most important things to observe in this example are as follows: •
• •
C# does not permit execution to fall through one case block to the next. Each case block must end with a statement that transfers control. This will be a break, goto. or return statement. Multiple case labels may be associated with a single block of code. The switch statement is case sensitive; in the example, "Cotton" and "COTTON" represent two different values.
2.4
Loops
C# provides four iteration statements: while, do, for, and foreach. The first three are the same constructs you find in C, C++, and Java; the foreach statement is designed to loop through collections of data such as arrays.
while loop Syntax: while ( boolean expression ) { body }
The statement(s) in the loop body are executed until the boolean expression is false. The loop does not execute if the expression is initially false.
Example: byte[] r = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF}; int ndx=0; int totVal = 0; while (ndx = 0; i--) { yield return stackCollection[i]; } } } public void Add(T item) { stackCollection[count] = item; count += 1; } // other class methods go here ...
This code should raise some obvious questions about iterators: Where is the implementation of IEnumerator? And how can a method with an IEnumerator return type or a property with an IEnumerable return type seemingly return a string value?
173
174
Chapter 4
■
Working with Objects in C#
The answer to these questions is that the compiler generates the code to take care of the details. If the member containing the yield return statement is an IEnumerable type, the compiler implements the necessary generics or non-generics version of both IEnumerable and IEnumerator; if the member is an IEnumerator type, it implements only the two enumerator interfaces. The developer’s responsibility is limited to providing the logic that defines how the collection is traversed and what items are returned. The compiler uses this logic to implement the IEnumerator.MoveNext method. The client code to access the GenStack collection is straightforward. An instance of the GenStack class is created to hold ten string elements. Three items are added to the collection and are then displayed in original and reverse sequence. GenStack myStack = new GenStack(10); myStack.Add("Aida"); myStack.Add("La Boheme"); myStack.Add("Carmen"); // uses enumerator from GetEnumerator() foreach (string s in myStack) Console.WriteLine(s); // uses enumerator from Reverse property foreach (string s in myStack.Reverse) Console.WriteLine(s);
The Reverse property demonstrates how easy it is to create multiple iterators for a collection. You simply implement a property that traverses the collection in some order and uses tbe yield return statement(s) to return an item in the collection.
Core Note In order to stop iteration before an entire collection is traversed, use the yield break keywords. This causes MoveNext() to return false.
There are some restrictions on using yield return or yield break: • • •
It can only be used inside a method, property (get accessor), or operator. It cannot be used inside a finally block, an anonymous method, or a method that has ref or out arguments. The method or property containing yield return must have a return type of Collections.IEnumerable, Collections.Generic. IEnumerable, Collections.IEnumerator, or Collections. Generic.IEnumerator.
4.4 Working with .NET Collection Classes and Interfaces
Using the IComparable and IComparer Interfaces to Perform Sorting Chapter 2, “C# Language Fundamentals,” included a discussion of the System.Array object and its associated Sort method. That method is designed for primitive types such as strings and numeric values. However, it can be extended to work with more complex objects by implementing the IComparable and IComparer interfaces on the objects to be sorted.
IComparable Unlike the other interfaces in this section, IComparable is a member of the System namespace. It has only one member, the method CompareTo: int CompareTo(Object obj) Returned Value
Condition
Less than 0 0 Greater than 0
Current instance < obj Current instance = obj Current instance > obj
The object in parentheses is compared to the current instance of the object implementing CompareTo, and the returned value indicates the results of the comparison. Let’s use this method to extend the Chair class so that it can be sorted on its myPrice field. This requires adding the IComparable inheritance and implementing the CompareTo method. public class Chair : ICloneable, IComparable { private double myPrice; private string myVendor, myID; public Upholstery myUpholstery; //… Constructor and other code // Add implementation of CompareTo to sort in ascending int IComparable.CompareTo(Object obj) { if (obj is Chair) { Chair castObj = (Chair)obj; if (this.myPrice > castObj.myPrice) return 1; if (this.myPrice < castObj.myPrice) return -1; else return 0;
175
176
Chapter 4
■
Working with Objects in C#
// Reverse 1 and –1 to sort in descending order } throw new ArgumentException("object in not a Chair"); }
The code to sort an array of Chair objects is straightforward because all the work is done inside the Chair class: Chair[]chairsOrdered = new Chair[4]; chairsOrdered[0] = new Chair(150.0, "Lane","99-88"); chairsOrdered[1] = new Chair(250.0, "Lane","99-00"); chairsOrdered[2] = new Chair(100.0, "Lane","98-88"); chairsOrdered[3] = new Chair(120.0, "Harris","93-9"); Array.Sort(chairsOrdered); // Lists in ascending order of price foreach(Chair c in chairsOrdered) MessageBox.Show(c.ToString());
IComparer The previous example allows you to sort items on one field. A more flexible and realistic approach is to permit sorting on multiple fields. This can be done using an overloaded form of Array.Sort that takes an object that implements IComparer as its second parameter. IComparer is similar to IComparable in that it exposes only one member, Compare, that receives two objects. It returns a value of –1, 0, or 1 based on whether the first object is less than, equal to, or greater than the second. The first object is usually the array to be sorted, and the second object is a class implementing a custom Compare method for a specific object field. This class can be implemented as a separate helper class or as a nested class within the class you are trying to sort (Chair). This code creates a helper class that sorts the Chair objects by the myVendor field: public class CompareByVen : IComparer { public CompareByVen() { } int IComparer.Compare(object obj1, object obj2) { // obj1 contains array being sorted // obj2 is instance of helper sort class Chair castObj1 = (Chair)obj1; Chair castObj2 = (Chair)obj2; return String.Compare (castObj1.myVendor,castObj2.myVendor); } }
4.4 Working with .NET Collection Classes and Interfaces
If you refer back to the Chair class definition (refer to Figure 4-6 on page 166), you will notice that there is a problem with this code: myVendor is a private member and not accessible in this outside class. To make the example work, change it to public. A better solution, of course, is to add a property to expose the value of the field. In order to sort, pass both the array to be sorted and an instance of the helper class to Sort: Array.Sort(chairsOrdered,new CompareByVen());
In summary, sorting by more than one field is accomplished by adding classes that implement Compare for each sortable field.
System.Collections Namespace The classes in this namespace provide a variety of data containers for managing collections of data. As shown in Figure 4-8, it is useful to categorize them based on the primary interface they implement: ICollection, IList, or IDictionary. Collections
ICollection Stack Queue
IList ArrayList
IDictionary HashTable SortedList
Figure 4-8 Selected classes in System.Collections
The purpose of interfaces is to provide a common set of behaviors for classes. Thus, rather than look at the details of all the classes, we will look at the interfaces themselves as well as some representative classes that illustrate how the members of the interfaces are implemented. We’ve already looked at ICollection, so let’s begin with two basic collections that inherit from it.
Stack and Queue The Stack and the Queue are the simplest of the collection classes. Because they do not inherit from IList or IDictionary, they do not provide indexed or keyed access. The order of insertion controls how objects are retrieved from them. In a Stack, all insertions and deletions occur at one end of the list; in a Queue, insertions are made at one end and deletions at the other. Table 4-5 compares the two.
177
178
Chapter 4
■
Working with Objects in C#
Table 4-5 Stack and Queue—Selected Members and Features Description
Stack
Queue
Method of maintaining data
Last-in, first-out (LIFO)
First-in, first-out (FIFO)
Add an item
Push()
EnQueue()
Remove an item
Pop()
DeQueue()
Return the current item without removing it
Peek()
Peek()
Determine whether an item is in the collection
Includes()
Includes()
Constructors
Stack()
Queue()
• Empty stack with default capacity
• Default capacity and growth factor
Stack(ICollection)
Queue(ICollection)
• Stack is filled with received collection
• Filled with received collection
Stack(int)
Queue(int)
• Set stack to initial int capacity
• Set queue to initial int capacity Queue(int, float)
• Set initial capacity and growth factor
Stacks play a useful role for an application that needs to maintain state information in order to hold tasks to be “performed later.” The call stack associated with exception handling is a classic example; stacks are also used widely in text parsing operations. Listing 4-8 provides an example of some of the basic stack operations.
Listing 4-8
Using the Stack Container
public class ShowStack { public static void Main() { Stack myStack = new Stack(); myStack.Push(new Chair(250.0, "Adams Bros.", "87-00" )); myStack.Push(new Chair(100.0, "Broyhill","87-04" )); myStack.Push(new Chair(100.0, "Lane","86-09" )); PrintValues( myStack ); // Adams – Broyhill - Lane
4.4 Working with .NET Collection Classes and Interfaces
Listing 4-8
Using the Stack Container (continued)
// Pop top object and push a new one on stack myStack.Pop(); myStack.Push(new Chair(300.0, "American Chair")); Console.WriteLine(myStack.Peek().ToString()); // American } public static void PrintValues( IEnumerable myCollection ) { System.Collections.IEnumerator myEnumerator = myCollection.GetEnumerator(); while ( myEnumerator.MoveNext() ) Consle.WriteLine(myEnumerator.Current.ToString()); // Could list specific chair fields with // myChair = (Chair) myEnumerator.Current; } }
Three objects are added to the stack. PrintValues enumerates the stack and lists the objects in the reverse order they were added. The Pop method removes “Lane” from the top of the stack. A new object is pushed onto the stack and the Peek method lists it. Note that the foreach statement could also be used to list the contents of the Stack.
ArrayList The ArrayList includes all the features of the System.Array, but also extends it to include dynamic sizing and insertion/deletion of items at a specific location in the list. These additional features are defined by the IList interface from which ArrayList inherits.
IList Interface This interface, whose members are listed in Table 4-6, is used to retrieve the contents of a collection via a zero-based numeric index. This permits insertion and removal at random location within the list. The most important thing to observe about this interface is that it operates on object types. This means that an ArrayList—or any collection implementing IList—may contain types of any kind. However, this flexibility comes at a cost: Casting must be widely used in order to access the object’s contents, and value types must be converted to objects (boxed) in order to be stored in the collection. As we see shortly, C# 2.0 offers a new feature—called generics—that addresses both issues. However, the basic functionality of the ArrayList, as illustrated in this code segment, remains the same.
179
180
Chapter 4
■
Working with Objects in C#
ArrayList chairList = new ArrayList( ); // alternative: ArrayList chairList = new ArrayList(5); chairList.Add(new Chair(350.0, "Adams", "88-00")); chairList.Add(new Chair(380.0, "Lane", "99-33")); chairList.Add(new Chair(250.0, "Broyhill", "89-01")); PrintValues(chairList); // Adams – Lane - Broyhill // chairList.Insert(1,new Chair(100,"Kincaid")); chairList.RemoveAt(2); Console.WriteLine("Object Count: {0}",chairList.Count); // PrintValues(chairList); // Adams – Kincaid - Broyhill // Copy objects to an array Object chairArray = chairList.ToArray(); PrintValues((IEnumerable) chairArray); Table 4-6 IList Members Interface
Description
bool IsFixedSize
Indicates whether the collection has a fixed size. A fixed size prevents the addition or removal of items after the collection is created.
bool IsReadOnly
Items in a collection can be read but not modified.
int
IndexOf(object)
Determines the index of a specific item in the collection.
int
Add(object)
Adds an item to the end of a list. It returns the value of the index where the item was added.
void Insert (index, object) void RemoveAt (index) void Remove (object)
Methods to insert a value at a specific index; delete the value at a specific index; and remove the first occurrence of an item having the specified value.
void Clear()
Remove all items from a collection.
bool Contains(object)
Returns true if a collection contains an item with a specified value.
This parameterless declaration of ArrayList causes a default amount of memory to be initially allocated. You can control the initial allocation by passing a size parameter to the constructor that specifies the number of elements you expect it to hold. In both cases, the allocated space is automatically doubled if the capacity is exceeded.
4.4 Working with .NET Collection Classes and Interfaces
Hashtable The Hashtable is a .NET version of a dictionary for storing key-value pairs. It associates data with a key and uses the key (a transformation algorithm is applied) to determine a location where the data is stored in the table. When data is requested, the same steps are followed except that the calculated memory location is used to retrieve data rather than store it. Syntax: public class Hashtable : IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback, ICloneable
As shown here, the Hashtable inherits from many interfaces; of these, IDictionary is of the most interest because it provides the properties and methods used to store and retrieve data.
IDictionary Interface Collections implementing the IDictionary interface contain items that can be retrieved by an associated key value. Table 4-7 summarizes the most important members for working with such a collection. Table 4-7 IDictionary Member Member
Description
bool IsFixedSize
Indicates whether IDictionary has a fixed size. A fixed size prevents the addition or removal of items after the collection is created.
bool IsReadOnly
Elements in a collection can be read but not modified.
ICollection Keys ICollection Values
Properties that return the keys and values of the collection.
void Add (key, value) void Clear () void Remove (key)
Methods to add a key-value pair to a collection, remove a specific key, and remove all items (clear) from the collection.
bool Contains
Returns true if a collection contains an element with a specified key.
IDictionaryEnumerator GetEnumerator ()
Returns an instance of the IDictionaryEnumerator type that is required for enumerating through a dictionary.
181
182
Chapter 4
■
Working with Objects in C#
IDictionaryEnumerator Interface As shown in Figure 4-7 on page 169, IDictionaryEnumerator inherits from IEnumerator. It adds properties to enumerate through a dictionary by retrieving keys, values, or both. Table 4-8 IDictionaryEnumerator Members Member DictionaryEntry
Description Entry
object Key object Value
The variable Entry is used to retrieve both the key and value when iterating through a collection. Properties that return the keys and values of the current collection entry.
All classes derived from IDictionary maintain two internal lists of data: one for keys and one for the associated value, which may be an object. The values are stored in a location based on the hash code of the key. This code is provided by the key’s System.Object.GetHashCode method, although it is possible to override this with your own hash algorithm. This structure is efficient for searching, but less so for insertion. Because keys may generate the same hash code, a collision occurs that requires the code be recalculated until a free bucket is found. For this reason, the Hashtable is recommended for situations where a large amount of relatively static data is to be searched by key values.
Create a Hashtable A parameterless constructor creates a Hashtable with a default number of buckets allocated and an implicit load factor of 1. The load factor is the ratio of values to buckets the storage should maintain. For example, a load factor of .5 means that a hash table should maintain twice as many buckets as there are values in the table. The alternate syntax to specify the initial number of buckets and load factor is Hashtable chairHash = new Hashtable(1000, .6)
The following code creates a hash table and adds objects to it: // Create HashTable Hashtable chairHash = new Hashtable(); // Add key - value pair to Hashtable chairHash.Add ("88-00", new Chair(350.0, "Adams", "88-00"); chairHash.Add ("99-03", new Chair(380.0, "Lane", "99-03");
4.4 Working with .NET Collection Classes and Interfaces
// or this syntax chairHash["89-01"] = new Chair(250.0, "Broyhill", "89-01");
There are many ways to add values to a Hashtable, including loading them from another collection. The preceding example shows the most straightforward approach. Note that a System.Argument exception is thrown if you attempt to add a value using a key that already exists in the table. To check for a key, use the ContainsKey method: // Check for existence of a key bool keyFound; if (chairHash.ContainsKey("88-00")) { keyFound = true;} else {keyFound = false;}
List Keys in a Hashtable The following iterates through the Keys property collection: // List Keys foreach (string invenKey in chairHash.Keys) { MessageBox.Show(invenKey); }
List Values in a Hashtable These statements iterate through the Values in a hash table: // List Values foreach (Chair chairVal in chairHash.Values) { MessageBox.Show(chairVal.myVendor);}
List Keys and Values in a Hashtable This code lists the keys and values in a hash table: foreach ( DictionaryEntry deChair in chairHash) { Chair obj = (Chair) deChair.Value; MessageBox.Show(deChair.Key+" "+ obj.myVendor); }
183
184
Chapter 4
■
Working with Objects in C#
The entry in a Hashtable is stored as a DictionaryEntry type. It has a Value and Key property that expose the actual value and key. Note that the value is returned as an object that must be cast to a Chair type in order to access its fields.
Core Note According to .NET documentation (1.x), a synchronized version of a Hashtable that is supposed to be thread-safe for a single writer and concurrent readers can be created using the Synchronized method: Hashtable safeHT = Hashtable.Synchronized(newHashtable());
Unfortunately, the .NET 1.x versions of the Hashtable have been proven not to be thread-safe for reading. Later versions may correct this flaw.
This section has given you a flavor of working with System.Collections interfaces and classes. The classes presented are designed to meet most general-purpose programming needs. There are numerous other useful classes in the namespace as well as in the System.Collections.Specialized namespace. You should have little trouble working with either, because all of their classes inherit from the same interfaces presented in this section.
System.Collections.Generic Namespace Recall from Chapter 3 that generics are used to implement type-safe classes, structures, and interfaces. The declaration of a generic type includes one (or more) type parameters in brackets () that serve(s) as a placeholder for the actual type to be used. When an instance of this type is created, the client uses these parameters to pass the specific type of data to be used in the generic type. Thus, a single generic class can handle multiple types of data in a type-specific manner. No classes benefit more from generics than the collections classes, which stored any type of data as an object in .NET 1.x. The effect of this was to place the burden of casting and type verification on the developer. Without such verification, a single ArrayList instance could be used to store a string, an integer, or a custom object. Only at runtime would the error be detected. The System.Collections.Generic namespace provides the generic versions of the classes in the System.Collections namespace. If you are familiar with the non-generic classes, switching to the generic type is straightforward. For example, this code segment using the ArrayList:
4.4 Working with .NET Collection Classes and Interfaces
ArrayList primes = new ArrayList(); primes.Add(1); primes.Add(3); int pSum = (int)primes[0] + (int)primes[1]; primes.Add("test");
can be replaced with the generics version: List primes = new List(); primes.Add(1); primes.Add(3); int pSum = primes[0] + primes[1]; primes.Add("text"); // will not compile
The declaration of List includes a type parameter that tells the compiler what type of data the object may contain—int in this case. The compiler then generates code that expects the specified type. For the developer, this eliminates the need for casting and type verification at runtime. From a memory usage and efficiency standpoint, it also eliminates boxing (conversion to objects) when primitives are stored in the collection.
Comparison of System.Collections and System.Collections.Generic Namespaces As the following side-by-side comparison shows, the classes in the two namespaces share the same name with three exceptions: Hashtable becomes Dictionary, ArrayList becomes List, and SortedList is renamed SortedDictionary. System.Collections
System.Collections.Generic
Comparer Hashtable ArrayList Queue SortedList Stack ICollection IComparable IComparer IDictionary IEnumerable IEnumerator IKeyComparer IList
Comparer Dictionary List Queue SortedDictionary Stack ICollection IComparable IComparer IDictionary IEnumerable IEnumerator IKeyComparer IList LinkedList
(not applicable)
185
186
Chapter 4
■
Working with Objects in C#
The only other points to note regard IEnumerator. Unlike the original version, the generics version inherits from IDisposable and does not support the Reset method.
An Example Using a Generics Collections Class Switching to the generics versions of the collection classes is primarily a matter of getting used to a new syntax, because the functionality provided by the generics and non-generics classes is virtually identical. To demonstrate, here are two examples. The first uses the Hashtable to store and retrieve instances of the Chair class (defined in Listing 4-5); the second example performs the same functions but uses the Dictionary class—the generics version of the Hashtable. This segment consists of a Hashtable declaration and two methods: one to store a Chair object in the table and the other to retrieve an object based on a given key value. // Example 1: Using Hashtable public Hashtable ht = new Hashtable(); // Store Chair object in table using a unique product identifier private void saveHT(double price, string ven, string sku) { if (!ht.ContainsKey(sku)) { ht.Add(sku, new Chair(price,ven,sku)); } } // Display vendor and price for a product identifier private void showChairHT(string sku) { if (ht.ContainsKey(key)) { if (ht[key] is Chair) // Prevent casting exception { Chair ch = (Chair)ht[sku]; Console.WriteLine(ch.MyVen + " " + ch.MyPr); } else { Console.WriteLine("Invalid Type: " + (ht[key].GetType().ToString())); } } }
Observe how data is retrieved from the Hashtable. Because data is stored as an object, verification is required to ensure that the object being retrieved is a Chair
4.5 Object Serialization
type; casting is then used to access the members of the object. These steps are unnecessary when the type-safe Dictionary class is used in place of the Hashtable. The Dictionary class accepts two type parameters that allow it to be strongly typed: K is the key type and V is the type of the value stored in the collection. In this example, the key is a string representing the unique product identifier, and the value stored in the Dictionary is a Chair type. // Example 2: Using Generics Dictionary to replace Hashtable // Dictionary accepts string as key and Chair as data type Dictionary htgen = new Dictionary(); // private void saveGen(double price, string ven, string sku) { if (!htgen.ContainsKey(sku)) { htgen.Add(sku, new Chair(price,ven,sku)); } } private void showChairGen(string sku) { if (htgen.ContainsKey(key)) { Chair ch = htgen[sku]; // No casting required Console.WriteLine(ch.MyVen + " " + ch.MyPr); } }
The important advantage of generics is illustrated in the showChairGen method. It has no need to check the type of the stored object or perform casting. In the long run, the new generic collection classes will render the classes in the System.Collections namespace obsolete. For that reason, new code development should use the generic classes where possible.
4.5
Object Serialization
In .NET, serialization refers to the process of converting an object or collection of objects into a format suitable for streaming across a network—a Web Service, for example—or storing in memory, a file, or a database. Deserialization is the reverse process that takes the serialized stream and converts it back into its original object(s).
187
188
Chapter 4
■
Working with Objects in C#
.NET support three primary types of serialization: • •
•
Binary. Uses the BinaryFormatter class to serialize a type into a binary stream. SOAP. Uses the SoapFormatter class to serialize a type into XML formatted according to SOAP (Simple Object Access Protocol) standards. XML. Uses the XmlSerializer class to serialize a type into basic XML (described in Chapter 10, “Working with XML in .NET”). Web Services uses this type of serialization.
Serialization is used primarily for two tasks: to implement Web Services and to store (persist) collections of objects to a medium from which they can be later resurrected. Web Services and their use of XML serialization are discussed in Chapter 18, “XML Web Services.” This section focuses on how to use binary serialization to store and retrieve objects. The examples use File I/O (see Chapter 5) methods to read and write to a file, which should be easily understood within the context of their usage.
Binary Serialization The BinaryFormatter object that performs binary serialization is found in the System.Runtime.Serialization.Formatters.Binary namespace. It performs serialization and deserialization using the Serialize and Deserialize methods, respectively, which it inherits from the IFormatter interface. Listing 4-9 provides an example of binary serialization using simple class members. A hash table is created and populated with two Chair objects. Next, a FileStream object is instantiated that points to a file on the local drive where the serialized output is stored. A BinaryFormatter is then created, and its Serialize method is used to serialize the hash table’s contents to a file. To confirm the process, the hash table is cleared and the BinaryFormatter object is used to deserialize the contents of the file into the hash table. Finally, one of the members from a restored object in the hash table is printed—verifying that the original contents have been restored.
Listing 4-9 using using using using
Serialization of a Hashtable
System; System.Runtime.Serialization; System.Runtime.Serialization.Formatters.Binary; System.IO;
4.5 Object Serialization
Listing 4-9
Serialization of a Hashtable (continued)
// Store Chair objects in a Hashtable Hashtable ht = new Hashtable(); // Chair and Upholstery must have [Serializable] attribute Chair ch = new Chair(100.00D, "Broyhill", "10-09"); ch.myUpholstery = new Upholstery("Cotton"); ht.Add("10-09", ch); // Add second item to table ch = new Chair(200.00D, "Lane", "11-19"); ch.myUpholstery = new Upholstery("Silk"); ht.Add("11-19", ch); // (1) Serialize // Create a new file; if file exits it is overwritten FileStream fs= new FileStream("c:\\chairs.dat", FileMode.Create); BinaryFormatter bf= new BinaryFormatter(); bf.Serialize(fs,ht); fs.Close(); // (2) Deserialize binary file into a Hashtable of objects ht.Clear(); // Clear hash table. fs = new FileStream("c:\\chairs.dat", FileMode.Open); ht = (Hashtable) bf.Deserialize(fs); // Confirm objects properly recreated ch = (Chair)ht["11-19"]; Console.WriteLine(ch.myUpholstery.Fabric); // "Silk" fs.Close();
Observe the following key points: • •
•
The serialization and IO namespaces should be declared. The Chair and Upholstery classes must have the [Serializable] attribute; otherwise, a runtime error occurs when Serialize()is executed. Serialization creates an object graph that includes references from one object to another. The result is a deep copy of the objects. In this example, the myUpholstery field of Chair is set to an instance of the Upholstery class before it is serialized. Serialization stores a copy of the object—rather than a reference. When deserialization occurs, the Upholstery object is restored.
189
190
Chapter 4
■
Working with Objects in C#
Excluding Class Members from Serialization You can selectively exclude class members from being serialized by attaching the [NonSerialized] attribute to them. For example, you can prevent the myUpholstery field of the Chair class from being serialized by including this: [NonSerialized] public Upholstery myUpholstery;
The primary reason for marking a field NonSerialized is that it may have no meaning where it is serialized. Because an object graph may be loaded onto a machine different from the one on which it was stored, types that are tied to system operations are the most likely candidates to be excluded. These include delegates, events, file handles, and threads.
Core Note A class cannot inherit the Serializable attribute from a base class; it must explicitly include the attribute. On the other hand, a derived class can be made serializable only if its base class is serializable.
Binary Serialization Events .NET 2.0 introduced support for four binary serialization and deserialization events, as summarized in Table 4-9. Table 4-9 Serialization and Deserialization Events Event
Attribute
Description
OnSerializing
[Serializing]
Occurs before objects are serialized. Event handler is called for each object to be serialized.
OnSerialized
[Serialized]
Occurs after objects are serialized. Event handler is called once for each object serialized.
OnDeserializing
[Deserializing]
Occurs before objects are deserialized. Event handler is called once for each object to be deserialized.
OnDeserialized
[Deserialized]
Occurs after objects have been deserialized. Event handler is called for each deserialized object.
4.5 Object Serialization
An event handler for these events is implemented in the object being serialized and must satisfy two requirements: the attribute associated with the event must be attached to the method, and the method must have this signature: void (StreamingContext context)
To illustrate, here is a method called after all objects have been deserialized. The binary formatter iterates the list of objects in the order they were deserialized and calls each object’s OnDeserialized method. This example uses the event handler to selectively update a field in the object. A more common use is to assign values to fields that were not serialized. public class Chair { // other code here [OnDeserialized] void OnDeserialized(StreamingContext context) { // Edit vendor name after object is created if (MyVen == "Lane") MyVen = "Lane Bros."; } }
Note that more than one method can have the same event attribute, and that more than one attribute can be assigned to a method—although the latter is rarely practical.
Handling Version Changes to a Serialized Object Suppose the Chair class from the preceding examples is redesigned. A field could be added or deleted, for example. What happens if one then attempts to deserialize objects in the old format to the new format? It’s not an uncommon problem, and .NET offers some solutions. If a field is deleted, the binary formatter simply ignores the extra data in the deserialized stream. However, if the formatter detects a new field in the target object, it throws an exception. To prevent this, an [OptionalField] attribute can be attached to the new field(s). Continuing the previous example, let’s add a field to Chair that designates the type of wood finish: [OptionalField] private string finish;
The presence of the attribute causes the formatter to assign a default null value to the finish field, and no exception is thrown. The application may also take advantage of the deserialized event to assign a value to the new field:
191
192
Chapter 4
■
Working with Objects in C#
void OnDeserialized(StreamingContext context) { if (MyVen == "Lane") finish = "Oak"; else finish = "Cherry"; }
4.6
Object Life Cycle Management
Memory allocation and deallocation have always been the bane of developers. Even the most experienced C++ and COM programmer is faced with memory leaks and attempts to access nonexistent objects that either never existed or have already been destroyed. In an effort to remove these responsibilities from the programmer, .NET implements a memory management system that features a managed heap and automatic Garbage Collection. Recall from Chapter 2, “C# Language Fundamentals,” that the managed heap is a pre-allocated area of memory that .NET uses to store reference types and data. Each time an instance of a class is created, it receives memory from the heap. This is a faster and cleaner solution than programming environments that rely on the operating system to handle memory allocation. Allocating memory from the stack is straightforward: A pointer keeps track of the next free memory address and allocates memory from the top of the heap. The important thing to note about the allocated memory is that it is always contiguous. There is no fragmentation or complex overhead to keep track of free memory blocks. Of course, at some point the heap is exhausted and unused space must be recovered. This is where the .NET automatic Garbage Collection comes into play.
.NET Garbage Collection Each time a managed object is created, .NET keeps track of it in a tree-like graph of nodes that associates each object with the object that created or uses it. In addition, each time another client references an object or a reference is assigned to a variable, the graph is updated. At the top of this graph is a list of roots, or parts of the application that exist as long at the program is running (see Figure 4-9). These include static variables, CPU registers, and any local or parameter variables that refer to objects on the managed heap. These serve as the starting point from which the .NET Framework uses a reference-tracing technique to remove objects from the heap and reclaim memory. The Garbage Collection process begins when some memory threshold is reached. At this point, the Garbage Collector (GC) searches through the graph of objects and marks those that are “reachable.” These are kept alive while the unreachable ones are considered to be garbage. The next step is to remove the unreferenced objects
4.6 Object Life Cycle Management
(garbage) and compact the heap memory. This is a complicated process because the collector must deal with the twin tasks of updating all old references to the new object addresses and ensuring that the state of the heap is not altered as Garbage Collection takes place. Managed Heap
Finalization Queue
References to Objects
Roots Object J Object I Object H Object G Object F Object E Object D Object C Object B Object A
Managed Heap
Finalization Queue
Object G Object F Object D Object B Freachable Queue
Before Garbage Collection
Figure 4-9
Object F Object B Object J Object I Object G Object F Object E Object D Object B
Freachable Queue
Object D Object G
After Garbage Collection
.NET Garbage Collection process
The details of Garbage Collection are not as important to the programmer as the fact that it is a nondeterministic (occurs unpredictably) event that deals with managed resources only. This leaves the programmer facing two problems: how to dispose of unmanaged resources such as files or network connections, and how to dispose of them in a timely manner. The solution to the first problem is to implement a method named Finalize that manages object cleanup; the second is solved by adding a Dispose method that can be called to release resources before Garbage Collection occurs. As we will see, these two methods do not operate autonomously. Proper object termination requires a solution that coordinates the actions of both methods.
Core Note Garbage Collection typically occurs when the CLR detects that some memory threshold has been reached. However, there is a static method GC.Collect that can be called to trigger Garbage Collection. It can be useful under controlled conditions while debugging and testing, but should not be used as part of an application.
193
194
Chapter 4
■
Working with Objects in C#
Object Finalization Objects that contain a Finalize method are treated differently during both object creation and Garbage Collection than those that do not contain a Finalize method. When an object implementing a Finalize method is created, space is allocated on the heap in the usual manner. In addition, a pointer to the object is placed in the finalization queue (see Figure 4-9). During Garbage Collection, the GC scans the finalization queue searching for pointers to objects that are no longer reachable. Those found are moved to the freachable queue. The objects referenced in this queue remain alive, so that a special background thread can scan the freachable queue and execute the Finalize method on each referenced object. The memory for these objects is not released until Garbage Collection occurs again. To implement Finalize correctly, you should be aware of several issues: • •
• •
Finalization degrades performance due to the increased overhead. Only use it when the object holds resources not managed by the CLR. Objects may be placed in the freachable queue in any order. Therefore, your Finalize code should not reference other objects that use finalization, because they may have already been processed. Call the base Finalize method within your Finalize method so it can perform any cleanup: base.Finalize(). Finalization code that fails to complete execution prevents the background thread from executing the Finalize method of any other objects in the queue. Infinite loops or synchronization locks with infinite timeouts are always to be avoided, but are particularly deleterious when part of the cleanup code.
It turns out that you do not have to implement Finalize directly. Instead, you can create a destructor and place the finalization code in it. The compiler converts the destructor code into a Finalize method that provides exception handling, includes a call to the base class Finalize, and contains the code entered into the destructor: Public class Chair { public Chair() { } ~Chair() // Destructor { // finalization code } }
Note that an attempt to code both a destructor and Finalize method results in a compiler error. As it stands, this finalization approach suffers from its dependency on the GC to implement the Finalize method whenever it chooses. Performance and scalability
4.6 Object Life Cycle Management
are adversely affected when expensive resources cannot be released when they are no longer needed. Fortunately, the CLR provides a way to notify an object to perform cleanup operations and make itself unavailable. This deterministic finalization relies on a public Dispose method that a client is responsible for calling.
IDisposable.Dispose() Although the Dispose method can be implemented independently, the recommended convention is to use it as a member of the IDisposable interface. This allows a client to take advantage of the fact that an object can be tested for the existence of an interface. Only if it detects IDisposable does it attempt to call the Dispose method. Listing 4-10 presents a general pattern for calling the Dispose method.
Listing 4-10
Pattern for Calling Dispose()
public class MyConnections: IDisposable { public void Dispose() { // code to dispose of resources base.Dispose(); // include call to base Dispose() } public void UseResources() { } } // Client code to call Dispose() class MyApp { public static void Main() { MyConnections connObj; connObj = new MyConnections(); try { connObj.UseResources(); } finally // Call dispose() if it exists { IDisposable testDisp; testDisp = connObj as IDisposable; if(testDisp != null) { testDisp.Dispose(); } } }
195
196
Chapter 4
■
Working with Objects in C#
This code takes advantage of the finally block to ensure that Dispose is called even if an exception occurs. Note that you can shorten this code by replacing the try/finally block with a using construct that generates the equivalent code: Using(connObj) { connObj.UseResources() }
Using Dispose and Finalize When Dispose is executed, the object’s unmanaged resources are released and the object is effectively disposed of. This raises a couple of questions: First, what happens if Dispose is called after the resources are released? And second, if Finalize is implemented, how do we prevent the GC from executing it since cleanup has already occurred? The easiest way to handle calls to a disposed object’s Dispose method is to raise an exception. In fact, the ObjectDisposedException exception is available for this purpose. To implement this, add a boolean property that is set to true when Dispose is first called. On subsequent calls, the object checks this value and throws an exception if it is true. Because there is no guarantee that a client will call Dispose, Finalize should also be implemented when resource cleanup is required. Typically, the same cleanup method is used by both, so there is no need for the GC to perform finalization if Dispose has already been called. The solution is to execute the SuppressFinalize method when Dispose is called. This static method, which takes an object as a parameter, notifies the GC not to place the object on the freachable queue. Listing 4-11 shows how these ideas are incorporated in actual code.
Listing 4-11
Pattern for Implementing Dispose() and Finalize()
public class MyConnections: IDisposable { private bool isDisposed = false; protected bool Disposed { get{ return isDisposed;} } public void Dispose() { if (isDisposed == false) {
4.6 Object Life Cycle Management
Listing 4-11
Pattern for Implementing Dispose() and Finalize() (continued)
CleanUp(); IsDisposed = true; GC.SuppressFinalize(this); } } protected virtual void CleanUp() { // cleanup code here } ~MyConnections() // Destructor that creates Finalize() { CleanUp(); } public void UseResources() { // code to perform actions if(Disposed) { throw new ObjectDisposedException ("Object has been disposed of"); } } } // Inheriting class that implements its own cleanup public class DBConnections: MyConnections { protected override void CleanUp() { // implement cleanup here base.CleanUp(); } }
The key features of this code include the following: •
•
A common method, CleanUp, has been introduced and is called from both Dispose and Finalize . It is defined as protected and virtual, and contains no concrete code. Classes that inherit from the base class MyConnections are responsible for implementing the CleanUp. As part of this, they must be sure to call the Cleanup method of the base class. This ensures that cleanup code runs on all levels of the class hierarchy.
197
198
Chapter 4
•
■
Working with Objects in C#
The read-only property Disposed has been added and is checked before methods in the base class are executed.
In summary, the .NET Garbage Collection scheme is designed to allow programmers to focus on their application logic and not deal with details of memory allocation and deallocation. It works well as long as the objects are dealing with managed resources. However, when there are valuable unmanaged resources at stake, a deterministic method of freeing them is required. This section has shown how the Dispose and Finalize methods can be used in concert to manage this aspect of an object’s life cycle.
4.7
Summary
This chapter has discussed how to work with objects. We’ve seen how to create them, manipulate them, clone them, group them in collections, and destroy them. The chapter began with a description of how to use a factory design pattern to create objects. It closed with a look at how object resources are released through automatic Garbage Collection and how this process can be enhanced programmatically through the use of the Dispose and Finalize methods. In between, the chapter examined how to make applications more robust with the use of intelligent exception handling, how to customize the System.Object methods such as Equals and ToString to work with your own objects, how cloning can be used to make deep or shallow copies, and how to use the built-in classes available in the System.Collections and System.Collections.Generic namespaces. As a by-product of this chapter, you should now have a much greater appreciation of the important role that interfaces play in application design. They represent the base product when constructing a class factory, and they allow you to clone (ICloneable), sort (IComparer), or enumerate (IEnumerable) custom classes. Knowing that an object implements a particular interface gives you an immediate insight into the capabilities of the object.
4.8
Test Your Understanding
1. What are the advantages of using a class factory to create objects? 2. Which class should custom exceptions inherit from? Which constructors should be included?
4.8 Test Your Understanding
3. How does the default System.Object.Equals method determine if two objects are equal? 4. Which interface(s) must a class implement in order to support the foreach statement? 5. What is the main advantage of using generics? 6. What is the purpose of implementing IDisposable on a class? 7. Refer to the following code: public class test: ICloneable { public int Age; public string Name; public test(string myname) { Name = myname; } public Object Clone() { return MemberwiseClone(); } } // Create instances of class test myTest = new test("Joanna"); myTest.Age = 36; test clone1 = (test) mytest.Clone(); test clone2 = myTest;
Indicate whether the following statements evaluate to true or false: a. Object.ReferenceEquals(myTest.Name, clone1.Name) b. Object.ReferenceEquals(myTest.Age, clone1.Age) c. myTest.Name = "Julie"; Object.ReferenceEquals(myTest.Name, clone1.Name)
d. Object.ReferenceEquals(myTest.Name, clone2.Name) 8. How does implementing Finalize on an object affect its Garbage Collection?
199
CREATING APPLICATIONS USING THE .NET FRAMEWORK CLASS LIBRARY
II ■
■
■
■
■
■
Chapter 5 C# Text Manipulation and File I/O
202
Chapter 6 Building Windows Forms Applications Chapter 7 Windows Forms Controls
318
Chapter 8 .NET Graphics Using GDI+ Chapter 9 Fonts, Text, and Printing
266
378
426
Chapter 10 Working with XML in .NET
460
■
Chapter 11 ADO.NET 496
■
Chapter 12 Data Binding with Windows Forms Controls
544
C# TEXT MANIPULATION AND FILE I/O
Topics in This Chapter • Characters and Unicode: By default, .NET stores a character as a 16-bit Unicode character. This enables an application to support international character sets—a technique referred to as localization. • String Overview: In .NET, strings are immutable. To use strings efficiently, it is necessary to understand what this means and how immutability affects string operations. • String Operations: In addition to basic string operations, .NET supports advanced formatting techniques for numbers and dates. • StringBuilder: The StringBuilder class offers an efficient alternative to concatenation for constructing screens. • Regular Expressions: The .NET Regex class provides an engine that uses regular expressions to parse, match, and extract values in a string. • Text Streams: Stream classes permit data to be read and written as a stream of bytes that can be encrypted and buffered. • Text Reading and Writing: The StreamReader and StreamWriter classes make it easy to read from and write to physical files. • System.IO: Classes in this namespace enable an application to work with underlying directory structure of the host operating system.
5
This chapter introduces the string handling capabilities provided by the .NET classes. Topics include how to use the basic String methods for extracting and manipulating string content; the use of the String.Format method to display numbers and dates in special formats; and the use of regular expressions (regexes) to perform advanced pattern matching. Also included is a look at the underlying features of .NET that influence how an application works with text. Topics include how the Just-In-Time (JIT) compiler optimizes the use of literal strings; the importance of Unicode as the cornerstone of character and string representations in .NET; and the built-in localization features that permit applications to automatically take into account the culture-specific characteristics of languages and countries. This chapter is divided into two major topics. The first topic focuses on how to create, represent, and manipulate strings using the System.Char, System.String, and Regex classes; the second takes up a related topic of how to store and retrieve string data. It begins by looking at the Stream class and how to use it to process raw bytes of data as streams that can be stored in files or transmitted across a network. The discussion then moves to using the TextReader/TextWriter classes to read and write strings as lines of text. The chapter concludes with examples of how members of the System.IO namespace are used to access the Microsoft Windows directory and file structure.
203
204
Chapter 5
5.1
■
C# Text Manipulation and File I/O
Characters and Unicode
One of the watershed events in computing was the introduction of the ASCII 7-bit character set in 1968 as a standardized encoding scheme to uniquely identify alphanumeric characters and other common symbols. It was largely based on the Latin alphabet and contained 128 characters. The subsequent ANSI standard doubled the number of characters—primarily to include symbols for European alphabets and currencies. However, because it was still based on Latin characters, a number of incompatible encoding schemes sprang up to represent non-Latin alphabets such as the Greek and Arabic languages. Recognizing the need for a universal encoding scheme, an international consortium devised the Unicode specification. It is now a standard, accepted worldwide, that defines a unique number for every character “no matter what the platform, no matter what the program, no matter what the language.”1
Unicode NET fully supports the Unicode standard. Its internal representation of a character is an unsigned 16-bit number that conforms to the Unicode encoding scheme. Two bytes enable a character to represent up to 65,536 values. Figure 5-1 illustrates why two bytes are needed. The uppercase character on the left is a member of the Basic Latin character set that consists of the original 128 ASCII characters. Its decimal value of 75 can be depicted in 8 bits; the unneeded bits are set to zero. However, the other three characters have values that range from 310 (0x0136) to 56,609 (0xDB05), which can be represented by no less than two bytes.
Figure 5-1
Unicode memory layout of a character
1. Unicode Consortium—www.unicode.org.
5.1 Characters and Unicode
Unicode characters have a unique identifier made up of a name and value, referred to as a code point. The current version 4.0 defines identifiers for 96,382 characters. These characters are grouped in over 130 character sets that include language scripts, symbols for math, music, OCR, geometric shapes, Braille, and many other uses. Because 16 bits cannot represent the nearly 100,000 characters supported worldwide, more bytes are required for some character sets. The Unicode solution is a mechanism by which two sets of 16-bit units define a character. This pair of code units is known as a surrogate pair. Together, this high surrogate and low surrogate represent a single 32-bit abstract character into which characters are mapped. This approach supports over 1,000,000 characters. The surrogates are constructed from values that reside in a reserved area at the high end of the Unicode code space so that they are not mistaken for actual characters. As a developer, you can pretty much ignore the details of whether a character requires 16 or 32 bits because the .NET API and classes handle the underlying details of representing Unicode characters. One exception to this—discussed later in this section—occurs if you parse individual bytes in a stream and need to recognize the surrogates. For this, .NET provides a special object to iterate through the bytes.
Core Note Unicode characters can only be displayed if your computer has a font supporting them. On a Windows operating system, you can install a font extension (ttfext.exe) that displays the supported Unicode ranges for a .ttf font. To use it, right-click the .ttf font name and select Properties. Console applications cannot print Unicode characters because console output always displays in a non-proportional typeface.
Working with Characters A single character is represented in .NET as a char (or Char) structure. The char structure defines a small set of members (see char in Chapter 2, “C# Language Fundamentals”) that can be used to inspect and transform its value. Here is a brief review of some standard character operations.
Assigning a Value to a Char Type The most obvious way to assign a value to a char variable is with a literal value. However, because a char value is represented internally as a number, you can also assign it a numeric value. Here are examples of each:
205
206
Chapter 5
■
C# Text Manipulation and File I/O
string klm = "KLM"; byte b = 75; char k; // Different ways to assign k = 'K'; k = klm[0]; // k = (char) 75; // k = (char) b; // k = Convert.ToChar(75); //
'K' to variable K Assign "K" from first value in klm Cast decimal cast byte Converts value to a char
Converting a Char Value to a Numeric Value When a character is converted to a number, the result is the underlying Unicode (ordinal) value of the character. Casting is the most efficient way to do this, although Convert methods can also be used. In the special case where the char is a digit and you want to assign the linguistic value—rather than the Unicode value—use the static GetNumericValue method. // '7' has Unicode value of 55 char k = '7'; int n = (int) k; // n = 55 n = (int) char.GetNumericValue(k); // n = 7
Characters and Localization One of the most important features of .NET is the capability to automatically recognize and incorporate culture-specific rules of a language or country into an application. This process, known as localization, may affect how a date or number is formatted, which currency symbol appears in a report, or how string comparisons are carried out. In practical terms, localization means a single application would display the date May 9, 2004 as 9/5/2004 to a user in Paris, France and as 5/9/2004 to a user in Paris, Texas. The Common Language Runtime (CLR) automatically recognizes the local computer’s culture and makes the adjustments. The .NET Framework provides more than a hundred culture names and identifiers that are used with the CultureInfo class to designate the language/country to be used with culture sensitive operations in a program. Although localization has a greater impact when working with strings, the Char.ToUpper method in this example is a useful way to demonstrate the concept. // Include the System.Globalization namespace // Using CultureInfo – Azerbaijan char i = 'i'; // Second parameter is false to use default culture settings // associated with selected culture CultureInfo myCI = new CultureInfo("az", false ); i = Char.ToUpper(i,myCI);
5.1 Characters and Unicode
An overload of ToUpper() accepts a CultureInfo object that specifies the culture (language and country) to be used in executing the method. In this case, az stands for the Azeri language of the country Azerbaijan (more about this follows). When the Common Language Runtime sees the CultureInfo parameter, it takes into account any aspects of the culture that might affect the operation. When no parameter is provided, the CLR uses the system’s default culture.
Core Note On a Windows operating system, the .NET Framework obtains its default culture information from the system’s country and language settings. It assigns these values to the Thread.CurrentThread.CurrentCulture property. You can set these options by choosing Regional Options in the Control Panel.
So why choose Azerbaijan, a small nation on the Caspian Sea, to demonstrate localization? Among all the countries in the world that use the Latin character set, only Azerbaijan and Turkey capitalize the letter i not with I (U+0049), but with an I that has a dot above it (U+0130). To ensure that ToUpper() performs this operation correctly, we must create an instance of the CultureInfo class with the Azeri culture name—represented by az—and pass it to the method. This results in the correct Unicode character—and a satisfied population of 8.3 million Azerbaijani.
Characters and Their Unicode Categories The Unicode Standard classifies Unicode characters into one of 30 categories. .NET provides a UnicodeCategory enumeration that represents each of these categories and a Char.GetUnicodecategory() method to return a character’s category. Here is an example: Char k = 'K'; int iCat = (int) char.GetUnicodeCategory(k); // 0 Console.WriteLine(char.GetUnicodeCategory(k)); // UppercaseLetter char cr = (Char)13; iCat = (int) char.GetUnicodeCategory(cr); // 14 Console.WriteLine(char.GetUnicodeCategory(cr)); // Control
The method correctly identifies K as an UppercaseLetter and the carriage return as a Control character. As an alternative to the unwieldy GetUnicodeCategory, char includes a set of static methods as a shortcut for identifying a character’s Unicode category. They are nothing more than wrappers that return a true or false value based on an internal call to GetUnicodeCategory. Table 5-1 lists these methods.
207
208
Chapter 5
■
C# Text Manipulation and File I/O
Table 5-1 Char Methods That Verify Unicode Categories
Method
Unicode Category
IsControl
4
Control code whose Unicode value is U+007F, or in the range U+0000 through U+001F, or U+0080 through U+009F.
IsDigit
8
Is in the range 0–9.
IsLetter
0, 1, 2, 4
Letter.
IsLetterorDigit
0, 1, 8,
Union of letters and digits.
IsLower
1
Lowercase letter.
IsUpper
0
Uppercase letter.
IsPunctuation
18, 19, 20, 21, 22, 23, 24
Punctuation symbol—for example, DashPunctuation(19) or OpenPunctuation(20), OtherPunctuation(24).
IsSeparator
11, 12, 13
Space separator, line separator, paragraph separator.
IsSurrogate
16
Value is a high or low surrogate.
IsSymbol
25, 26, 28
Symbol.
IsWhiteSpace
11
Whitespace can be any of these characters: space (0x20), carriage return (0x0D), horizontal tab (0x09), line feed (0x0A), form feed (0x0C), or vertical tab (0x0B).
Description
Using these methods is straightforward. The main point of interest is that they have overloads that accept a single char parameter, or two parameters specifying a string and index to the character within the string. Console.WriteLine(Char.IsSymbol('+')); Console.WriteLine(Char.IsPunctuation('+')): string str = "black magic"; Console.WriteLine(Char.IsWhiteSpace(str, 5)); char p = '.'; Console.WriteLine(Char.IsPunctuation(p)); Int iCat = (int) char.GetUnicodeCategory(p); Char p = '('; Console.WriteLine(Char.IsPunctuation(p)); int iCat = (int) char.GetUnicodeCategory(p);
// true // false // true // true // 24 // true // 20
5.2 The String Class
5.2
The String Class
The System.String class was introduced in Chapter 2. This section expands that discussion to include a more detailed look at creating, comparing, and formatting strings. Before proceeding to these operations, let’s first review the salient points from Chapter 2: •
• •
•
The System.String class is a reference type having value semantics. This means that unlike most reference types, string comparisons are based on the value of the strings and not their location. A string is a sequence of Char types. Any reference to a character within a string is treated as a char. Strings are immutable. This means that after a string is created, it cannot be changed at its current memory location: You cannot shorten it, append to it, or change a character within it. The string value can be changed, of course, but the modified string is stored in a new memory location. The original string remains until the Garbage Collector removes it. The System.Text.StringBuilder class provides a set of methods to construct and manipulate strings within a buffer. When the operations are completed, the contents are converted to a string. StringBuilder should be used when an application makes extensive use of concatenation and string modifications.
Creating Strings A string is created by declaring a variable as a string type and assigning a value to it. The value may be a literal string or dynamically created using concatenation. This is often a perfunctory process and not an area that most programmers consider when trying to improve code efficiency. In .NET, however, an understanding of how literal strings are handled can help a developer improve program performance.
String Interning One of the points of emphasis in Chapter 1, “Introduction to .NET and C#,” was to distinguish how value and reference types are stored in memory. Recall that value types are stored on a stack, whereas reference types are placed on a managed heap. It turns out that that the CLR also sets aside a third area in memory called the intern pool, where it stores all the string literals during compilation. The purpose of this pool is to eliminate duplicate string values from being stored.
209
210
Chapter 5
■
C# Text Manipulation and File I/O
Consider the following code: string string string string
poem1 poem2 poem3 poem4
= = = =
"Kubla Khan"; "Kubla Khan"; String.Copy(poem2); // Create new string object "Christabel";
Figure 5-2 shows a simplified view of how the strings and their values are stored in memory.
• • • •
poem1 poem2 poem3 poem4
Object3 "Christabel" Object2 "Kubla Khan"
Thread Stack
Object1 "Kubla Khan" Key
Pointer
"Christabel" "Kubla Khan"
Managed Heap
• •
Intern Pool Figure 5-2 String interning
The intern pool is implemented as a hash table. The hash table key is the actual string and its pointer references the associated string object on the managed heap. When the JITcompiler compiles the preceding code, it places the first instance of "Kubla Khan" (poem1) in the pool and creates a reference to the string object on the managed heap. When it encounters the second string reference to "Kubla Khan" (poem2), the CLR sees that the string already exists in memory and, instead of creating a new string, simply assigns poem2 to the same object as poem1. This process is known as string interning. Continuing with the example, the String.Copy method creates a new string poem3 and creates an object for it in the managed heap. Finally, the string literal associated with poem4 is added to the pool. To examine the practical effects of string interning, let’s extend the previous example. We add code that uses the equivalence (==) operator to compare string values and the Object.ReferenceEquals method to compare their addresses.
5.2 The String Class
Console.WriteLine(poem1 == poem2); // Console.WriteLine(poem1 == poem3); // Console.WriteLine(ReferenceEquals(poem1, poem3)); // Console.WriteLine(ReferenceEquals(poem1, "Kubla Khan")); //
true true false true
The first two statements compare the value of the variables and—as expected— return a true value. The third statement compares the memory location of the variables poem3 and poem2. Because they reference different objects in the heap, a value of false is returned. The .NET designers decided to exclude dynamically created values from the intern pool because checking the intern pool each time a string was created would hamper performance. However, they did include the String.Intern method as a way to selectively add dynamically created strings to the literal pool. string khan = " Khan"; string poem5 = "Kubla" + khan; Console.WriteLine(ReferenceEquals(poem5, poem1)); // false // Place the contents of poem5 in the intern pool—if not there poem5 = String.Intern(poem5); Console.WriteLine(ReferenceEquals(poem5, poem1)); // true
The String.Intern method searches for the value of poem5 ("Kubla Khan") in the intern pool; because it is already in the pool, there is no need to add it. The method returns a reference to the already existing object (Object1) and assigns it to poem5. Because poem5 and poem1 now point to the same object, the comparison in the final statement is true. Note that the original object created for poem5 is released and swept up during the next Garbage Collection.
Core Recommendation Use the String.Intern method to allow a string variable to take advantage of comparison by reference, but only if it is involved in numerous comparisons.
Overview of String Operations The System.String class provides a large number of static and instance methods, most of which have several overload forms. For discussion purposes, they can be grouped into four major categories based on their primary function:
211
212
Chapter 5
•
•
•
•
■
C# Text Manipulation and File I/O
String Comparisons. The String.Equals, String.Compare, and String.CompareOrdinal methods offer different ways to compare string values. The choice depends on whether an ordinal or lexical comparison is needed, and whether case or culture should influence the operation. Indexing and Searching. A string is an array of Unicode characters that may be searched by iterating through it as an array or by using special index methods to locate string values. String Transformations. This is a catchall category that includes methods for inserting, padding, removing, replacing, trimming, and splitting character strings. Formatting. NET provides format specifiers that are used in conjunction with String.Format to represent numeric and DateTime values in a number of standard and custom formats.
Many of the string methods—particularly for formatting and comparisons—are culture dependent. Where applicable, we look at how culture affects the behavior of a method.
5.3
Comparing Strings
The most efficient way to determine if two string variables are equal is to see if they refer to the same memory address. We did this earlier using the ReferenceEquals method. If two variables do not share the same memory address, it is necessary to perform a character-by-character comparison of the respective values to determine their equality. This takes longer than comparing addresses, but is often unavoidable. .NET attempts to optimize the process by providing the String.Equals method that performs both reference and value comparisons automatically. We can describe its operation in the following pseudo-code: If string1 and string2 reference the same memory location Then strings must be equal Else Compare strings character by character to determine equality
This code segment demonstrates the static and reference forms of the Equals method: string string string string
poem1 poem2 poem3 poem4
= = = =
"Kubla Khan"; "Kubla Khan"; String.Copy(poem2); "kubla khan";
5.3 Comparing Strings
// Console.WriteLine(String.Equals(poem1,poem2)); // true Console.WriteLine(poem1.Equals(poem3)); // true Console.WriteLine(poem1 == poem3); // equivalent to Equals Console.WriteLine(poem1 == poem4); // false – case differs
Note that the == operator, which calls the Equals method underneath, is a more convenient way of expressing the comparison. Although the Equals method satisfies most comparison needs, it contains no overloads that allow it to take case sensitivity and culture into account. To address this shortcoming, the string class includes the Compare method.
Using String.Compare String.Compare is a flexible comparison method that is used when culture or case
must be taken into account. Its many overloads accept culture and case-sensitive parameters, as well as supporting substring comparisons. Syntax: int Compare (string str1, string str2) Compare (string str1, string str2, bool IgnoreCase) Compare (string str1, string str2, bool IgnoreCase, CultureInfo ci) Compare (string str1, int index1, string str2, int index2, int len)
Parameters: str1 and str2
Specify strings to be compared.
IgnoreCase
Set true to make comparison case-insensitive (default is false).
index1 and index2
Starting position in str1 and str2.
ci
A CultureInfo object indicating the culture to be used.
Compare returns an integer value that indicates the results of the comparison. If the two strings are equal, a value of 0 is returned; if the first string is less than the second, a value less than zero is returned; if the first string is greater than the second, a value greater than zero is returned. The following segment shows how to use Compare to make case-insensitive and case-sensitive comparisons: int result; string stringUpper = "AUTUMN";
213
214
Chapter 5
■
C# Text Manipulation and File I/O
string stringLower = "autumn"; // (1) Lexical comparison: "A" is greater than "a" result = string.Compare(stringUpper,stringLower); // 1 // (2) IgnoreCase set to false result = string.Compare(stringUpper,stringLower,false); // 1 // (3)Perform case-insensitive comparison result = string.Compare(stringUpper,stringLower,true); // 0
Perhaps even more important than case is the potential effect of culture information on a comparison operation. .NET contains a list of comparison rules for each culture that it supports. When the Compare method is executed, the CLR checks the culture associated with it and applies the rules. The result is that two strings may compare differently on a computer with a US culture vis-à-vis one with a Japanese culture. There are cases where it may be important to override the current culture to ensure that the program behaves the same for all users. For example, it may be crucial that a sort operation order items exactly the same no matter where the application is run. By default, the Compare method uses culture information based on the Thread.CurrentThread.CurrentCulture property. To override the default, supply a CultureInfo object as a parameter to the method. This statement shows how to create an object to represent the German language and country: CultureInfo ci = new CultureInfo("de-DE");
// German culture
To explicitly specify a default culture or no culture, the CultureInfo class has two properties that can be passed as parameters—CurrentCulture, which tells a method to use the culture of the current thread, and InvariantCulture, which tells a method to ignore any culture. Let’s look at a concrete example of how culture differences affect the results of a Compare() operation. using System.Globalization;
// Required for CultureInfo
// Perform case-sensitive comparison for Czech culture string s1 = "circle"; string s2 = "chair"; result = string.Compare(s1, s2, true, CultureInfo.CurrentCulture)); // 1 result = string.Compare(s1, s2, true, CultureInfo.InvariantCulture)); // 1 // Use the Czech culture result = string.Compare(s1, s2, true, new CultureInfo("cs-CZ")); // -1
5.3 Comparing Strings
The string values "circle" and "chair" are compared using the US culture, no culture, and the Czech culture. The first two comparisons return a value indicating that "circle" > "chair", which is what you expect. However, the result using the Czech culture is the opposite of that obtained from the other comparisons. This is because one of the rules of the Czech language specifies that "ch" is to be treated as a single character that lexically appears after "c".
Core Recommendation When writing an application that takes culture into account, it is good practice to include an explicit CultureInfo parameter in those methods that accept such a parameter. This provides a measure of self-documentation that clarifies whether the specific method is subject to culture variation.
Using String.CompareOrdinal To perform a comparison that is based strictly on the ordinal value of characters, use String.CompareOrdinal. Its simple algorithm compares the Unicode value of two strings and returns a value less than zero if the first string is less than the second; a value of zero if the strings are equal; and a value greater than zero if the first string is greater than the second. This code shows the difference between it and the Compare method: string stringUpper = "AUTUMN"; string stringLower = "autumn"; // result = string.Compare(stringUpper,stringLower, false, CultureInfo.InvariantCulture); result = string.CompareOrdinal(stringUpper,stringLower);
// 1 // -32
Compare performs a lexical comparison that regards the uppercase string to be greater than the lowercase. CompareOrdinal examines the underlying Unicode values. Because A (U+0041) is less than a (U+0061), the first string is less than the second.
215
216
Chapter 5
5.4
■
C# Text Manipulation and File I/O
Searching, Modifying, and Encoding a String’s Content
This section describes string methods that are used to perform diverse but familiar tasks such as locating a substring within a string, changing the case of a string, replacing or removing text, splitting a string into delimited substrings, and trimming leading and trailing spaces.
Searching the Contents of a String A string is an implicit zero-based array of chars that can be searched using the array syntax string[n], where n is a character position within the string. For locating a substring of one or more characters in a string, the string class offers the IndexOf and IndexOfAny methods. Table 5-2 summarizes these. Table 5-2 Ways to Examine Characters Within a String String Member
Description
[ n ]
Indexes a 16-bit character located at position n within a string. int ndx= 0; while (ndx < poem.Length) { Console.Write(poem[ndx]); //Kubla Khan ndx += 1; }
IndexOf/LastIndexOf (string, [int start], [int count]) count. Number of chars to examine. IndexOfAny/LastIndexOfAny
Returns the index of the first/last occurrence of a specified string within an instance. Returns –1 if no match. string poem = "Kubla Khan"; int n = poem.IndexOf("la"); n = poem.IndexOf('K'); n = poem.IndexOf('K',4);
// 3 // 0 // 6
Returns the index of the first/last character in an array of Unicode characters. string poem = "Kubla Khan"; char[] vowels = new char[5] {'a', 'e', 'i', 'o', 'u'}; n = poem.IndexOfAny(vowels); // 1 n = poem.LastIndexOfAny(vowels); // 8 n = poem.IndexOfAny(vowels,2); // 4
5.4 Searching, Modifying, and Encoding a String’s Content
Searching a String That Contains Surrogates All of these techniques assume that a string consists of a sequence of 16-bit characters. Suppose, however, that your application must work with a Far Eastern character set of 32-bit characters. These are represented in storage as a surrogate pair consisting of a high and low 16-bit value. Clearly, this presents a problem for an expression such as poem[ndx], which would return only half of a surrogate pair. For applications that must work with surrogates, .NET provides the StringInfo class that treats all characters as text elements and can automatically detect whether a character is 16 bits or a surrogate. Its most important member is the GetTextElementEnumerator method, which returns an enumerator that can be used to iterate through text elements in a string. TextElementEnumerator tEnum = StringInfo.GetTextElementEnumerator(poem) ; while (tEnum.MoveNext()) // Step through the string { Console.WriteLine(tEnum.Current); // Print current char }
Recall from the discussion of enumerators in Chapter 4, “Working with Objects in C#,” that MoveNext() and Current are members implemented by all enumerators.
String Transformations Table 5-3 summarizes the most important string class methods for modifying a string. Because the original string is immutable, any string constructed by these methods is actually a new string with its own allocated memory. Table 5-3 Methods for Manipulating and Transforming Strings Tag
Description
Insert (int, string)
Inserts a string at the specified position. string mariner = "and he stoppeth three"; string verse = mariner.Insert( mariner.IndexOf(" three")," one of"); // verse --> "and he stoppeth one of three"
PadRight/PadLeft
Pads a string with a given character until it is a specified width. If no character is specified, whitespace is used. string rem = "and so on"; rem = rem.PadRight(rem.Length+3,'.'); // rem --> "and so on..."
217
218
Chapter 5
■
C# Text Manipulation and File I/O
Table 5-3 Methods for Manipulating and Transforming Strings (continued) Tag
Description
Remove(p , n)
Removes n characters beginning at position p. string verse = "It is an Ancient Mariner"; string newverse = (verse.Remove(0,9)); // newverse --> "Ancient Mariner"
Replace (A , B)
Replaces all occurrences of A with B, where A and B are chars or strings. string aString = "nap ace sap path"; string iString = aString.Replace('a','i'); // iString --> "nip ice sip pith"
Split( char[])
The char array contains delimiters that are used to break a string into substrings that are returned as elements in a string array. string words = "red,blue orange "; string [] split = words.Split(new Char [] {' ', ','}); Console.WriteLine(split[2]); // orange
ToUpper() ToUpper(CultureInfo) ToLower() ToLower(CultureInfo)
Returns an upper- or lowercase copy of the string.
Trim() Trim(params char[])
Removes all leading and trailing whitespaces. If a char array is provided, all leading and trailing characters in the array are removed.
string poem2="Kubla Khan"; poem2= poem2.ToUpper( CultureInfo.InvariantCulture);
string name = " Samuel Coleridge"; name = name.Trim(); // "Samuel Coleridge" TrimEnd (params char[]) TrimStart(params char[])
Removes all leading or trailing characters specified in a char array. If null is specified, whitespaces are removed. string name = " Samuel Coleridge"; trimName = name.TrimStart(null); shortname = name.TrimEnd('e','g','i'); // shortName --> "Samuel Colerid"
Substring(n) Substring(n, l)
Extracts the string beginning at a specified position (n) and of length l, if specified. string title="Kubla Khan"; Console.WriteLine(title.Substring(2,3)); //bla
5.4 Searching, Modifying, and Encoding a String’s Content
Table 5-3 Methods for Manipulating and Transforming Strings (continued) Tag
Description
ToCharArray() ToCharArray(n, l)
Extracts characters from a string and places in an array of Unicode characters. string myVowels = "aeiou"; char[] vowelArr; vowelArr = myVowels.ToCharArray(); Console.WriteLine(vowelArr[1]); // "e"
Most of these methods have analogues in other languages and behave as you would expect. Somewhat surprisingly, as we see in the next section, most of these methods are not available in the StringBuilder class. Only Replace, Remove, and Insert are included.
String Encoding Encoding comes into play when you need to convert between strings and bytes for operations such as writing a string to a file or streaming it across a network. Character encoding and decoding offer two major benefits: efficiency and interoperability. Most strings read in English consist of characters that can be represented by 8 bits. Encoding can be used to strip an extra byte (from the 16-bit Unicode memory representation) for transmission and storage. The flexibility of encoding is also important in allowing an application to interoperate with legacy data or third-party data encoded in different formats. The .NET Framework supports many forms of character encoding and decoding. The most frequently used include the following: •
•
•
UTF-8. Each character is encoded as a sequence of 1 to 4 bytes, based on its underlying value. ASCII compatible characters are stored in 1 byte; characters between 0x0080 and 0x07ff are stored in 2 bytes; and characters having a value greater than or equal to 0x0800 are converted to 3 bytes. Surrogates are written as 4 bytes. UTF-8 (which stands for UCS Transformation Format, 8-bit form) is usually the default for .NET classes when no encoding is specified. UTF-16. Each character is encoded as 2 bytes (except surrogates), which is how characters are represented internally in .NET. This is also referred to as Unicode encoding. ASCII. Encodes each character as an 8-bit ASCII character. This should be used when all characters are in the ASCII range (0x00 to 0x7F). Attempting to encode a character outside of the ACII range yields whatever value is in the character’s low byte.
219
220
Chapter 5
■
C# Text Manipulation and File I/O
Encoding and decoding are performed using the Encoding class found in the System.Text namespace. This abstract class has several static properties that return an object used to implement a specific encoding technique. These properties include ASCII, UTF8, and Unicode. The latter is used for UTF-16 encoding.
An encoding object offers several methods—each having several overloads—for converting between characters and bytes. Here is an example that illustrates two of the most useful methods: GetBytes, which converts a text string to bytes, and GetString, which reverses the process and converts a byte array to a string. string text= "In Xanadu did Kubla Khan"; Encoding UTF8Encoder = Encoding.UTF8; byte[] textChars = UTF8Encoder.GetBytes(text); Console.WriteLine(textChars.Length); // 24 // Store using UTF-16 textChars = Encoding.Unicode.GetBytes(text); Console.WriteLine(textChars.Length); // 48 // Treat characters as two bytes string decodedText = Encoding.Unicode.GetString(textChars); Console.WriteLine(decodedText); // "In Xanadu did ... "
You can also instantiate the encoding objects directly. In this example, the UTF-8 object could be created with UTF8Encoding UTF8Encoder = new UTF8Encoding();
With the exception of ASCIIEncoding, the constructor for these classes defines parameters that allow more control over the encoding process. For example, you can specify whether an exception is thrown when invalid encoding is detected.
5.5
StringBuilder
The primary drawback of strings is that memory must be allocated each time the contents of a string variable are changed. Suppose we create a loop that iterates 100 times and concatenates one character to a string during each iteration. We could end up with a hundred strings in memory, each differing from its preceding one by a single character. The StringBuilder class addresses this problem by allocating a work area (buffer) where its methods can be applied to the string. These methods include ways to append, insert, delete, remove, and replace characters. After the operations are complete, the ToString method is called to convert the buffer to a string that can be assigned to a string variable. Listing 5-1 introduces some of the StringBuilder methods in an example that creates a comma delimited list.
5.5 StringBuilder
Listing 5-1
Introduction to StringBuilder
using System; using System.Text; public class MyApp { static void Main() { // Create comma delimited string with quotes around names string namesF = "Jan Donna Kim "; string namesM = "Rob James"; StringBuilder sbCSV = new StringBuilder(); sbCSV.Append(namesF).Append(namesM); sbCSV.Replace(" ","','"); // Insert quote at beginning and end of string sbCSV.Insert(0,"'").Append("'"); string csv = sbCSV.ToString(); // csv = 'Jan','Donna','Kim','Rob','James' } }
All operations occur in a single buffer and require no memory allocation until the final assignment to csv. Let’s take a formal look at the class and its members.
StringBuilder Class Overview Constructors for the StringBuilder class accept an initial string value as well as integer values that specify the initial space allocated to the buffer (in characters) and the maximum space allowed. // Stringbuilder(initial value) StringBuilder sb1 = new StringBuilder("abc"); // StringBuilder(initial value, initial capacity) StringBuilder sb2 = new StringBuilder("abc", 16); // StringBuiler(Initial Capacity, maximum capacity) StringBuilder sb3 = new StringBuilder(32,128);
The idea behind StringBuilder is to use it as a buffer in which string operations are performed. Here is a sample of how its Append, Insert, Replace, and Remove methods work: int i = 4; char[] ch = {'w','h','i','t','e'}; string myColor = " orange";
221
222
Chapter 5
■
C# Text Manipulation and File I/O
StringBuilder sb = new StringBuilder("red blue green"); sb.Insert(0, ch); // whitered blue green sb.Insert(5," "); // white red blue green sb.Insert(0,i); // 4white red blue green sb.Remove(1,5); // 4 red blue green sb.Append(myColor); // 4 red blue green orange sb.Replace("blue","violet"); // 4 red violet green orange string colors = sb.ToString();
StringBuilder Versus String Concatenation Listing 5-2 tests the performance of StringBuilder versus the concatenation operator. The first part of this program uses the + operator to concatenate the letter a to a string in each of a loop’s 50,000 iterations. The second half does the same, but uses the StringBuilder.Append method. The Environment.TickCount provides the beginning and ending time in milliseconds.
Listing 5-2
Comparison of StringBuilder and Regular Concatenation
using System; using System.Text; public class MyApp { static void Main() { Console.WriteLine("String routine"); string a = "a"; string str = string.Empty; int istart, istop; istart = Environment.TickCount; Console.WriteLine("Start: "+istart); // Use regular C# concatenation operator for(int i=0; i 1/19/2004 5:05 PM Console.Writeline("Date: {0:g} ", curDate);
If none of the standard format specifiers meet your need, you can construct a custom format from a set of character sequences designed for that purpose. Table 5-6 lists some of the more useful ones for formatting dates. Table 5-6 Character Patterns for Custom Date Formatting Format
Description
Example
d
Day of month. No leading zero.
5
dd
Day of month. Always has two digits.
05
5.6 Formatting Numeric and DateTime Values
Table 5-6 Character Patterns for Custom Date Formatting (continued) Format
Description
Example
ddd
Day of week with three-character abbreviation.
Mon
dddd
Day of week full name.
Monday
M
Month number. No leading zero.
1
MM
Month number with leading zero if needed.
01
MMM
Month name with three-character abbreviation.
Jan
MMMM
Full name of month.
January
y
Year. Last one or two digits.
5
yy
Year. Last one or two digits with leading zero if needed.
05
yyyy
Four-digit year.
2004
HH
Hour in 24-hour format.
15
mm
Minutes with leading zero if needed.
20
Here are some examples of custom date formats: DateTime curDate = DateTime.Now; f = String.Format("{0:dddd} {0:MMM} {0:dd}", curDate); // output: Monday Jan 19 f = currDate.ToString("dd MMM yyyy") // output: 19 Jan 2004 // The standard short date format (d) is equivalent to this: Console.WriteLine(currDate.ToString("M/d/yyyy")); // 1/19/2004 Console.WriteLine(currDate.ToString("d")); // 1/19/2004 CultureInfo ci = new CultureInfo("de-DE"); f = currDate.ToString("dd-MMMM-yyyy HH:mm", ci) // output: 19-Januar-2004 23:07
// German
ToString is recommended over String.Format for custom date formatting. It has a more convenient syntax for embedding blanks and special separators between the date elements; in addition, its second parameter is a culture indicator that makes it easy to test different cultures.
229
230
Chapter 5
■
C# Text Manipulation and File I/O
Dates and Culture Dates are represented differently throughout the world, and the ability to add culture as a determinant in formatting dates shifts the burden to .NET from the developer. For example, if the culture on your system is German, dates are automatically formatted to reflect a European format: the day precedes the month; the day, month, and year are separated by periods (.) rather than slashes (/); and the phrase Monday, January 19 becomes Montag, 19. Januar. Here is an example that uses ToString with a German CultureInfo parameter: CultureInfo ci = new CultureInfo("de-DE"); Console.WriteLine(curDate.ToString("D",ci)); // output ---> Montag, 19. Januar 2004 Console.WriteLine(curDate.ToString("dddd",ci));
// German
// -->Montag
The last statement uses the special custom format "dddd" to print the day of the week. This is favored over the DateTime.DayofWeek enum property that returns only an English value.
NumberFormatInfo and DateTimeFormatInfo Classes These two classes govern how the previously described format patterns are applied to dates and numbers. For example, the NumberFormatInfo class includes properties that specify the character to be used as a currency symbol, the character to be used as a decimal separator, and the number of decimal digits to use when displaying a currency value. Similarly, DateTimeFormatInfo defines properties that correspond to virtually all of the standard format specifiers for dates. One example is the FullDateTimePattern property that defines the pattern returned when the character F is used to format a date. NumberFormatInfo and DateTimeFormatInfo are associated with specific cultures, and their properties are the means for creating the unique formats required by different cultures. .NET provides a predefined set of property values for each culture, but they can be overridden. Their properties are accessed in different ways depending on whether the current or non-current culture is being referenced (current culture is the culture associated with the current thread). The following statements reference the current culture: NumberFormatInfo.CurrentInfo. CultureInfo.CurrentCulture.NumberFormat.
The first statement uses the static property CurrentInfo and implicitly uses the current culture. The second statement specifies a culture explicitly (CurrentCulture) and is suited for accessing properties associated with a non-current CultureInfo instance.
5.6 Formatting Numeric and DateTime Values
CultureInfo ci = new CultureInfo("de-DE"); string f = ci.NumberFormat.CurrencySymbol; NumberFormatInfo and DateTimeFormatInfo properties associated with a non-current culture can be changed; those associated with the current thread are read-only. Listing 5-3 offers a sampling of how to work with these classes.
Listing 5-3
Using NumberFormatInfo and DateTimeFormatInfo
using System using System.Globalization Class MyApp { // NumberFormatInfo string curSym = NumberFormatInfo.CurrentInfo.CurrencySymbol; int dd = NumberFormatInfo.CurrentInfo.CurrencyDecimalDigits; int pdd = NumberFormatInfo.CurrentInfo.PercentDecimalDigits; // --> curSym = "$" dd = 2 pdd = 2 // DateTimeFormatInfo string ldp= DateTimeFormatInfo.CurrentInfo.LongDatePattern; // --> ldp = "dddd, MMMM, dd, yyyy" string enDay = DateTimeFormatInfo.CurrentInfo.DayNames[1]; string month = DateTimeFormatInfo.CurrentInfo.MonthNames[1]; CultureInfo ci = new CultureInfo("de-DE"); string deDay = ci.DateTimeFormat.DayNames[1]; // --> enDay = "Monday" month = February deDay = "Montag" // Change the default number of decimal places // in a percentage decimal passRate = .840M; Console.Write(passRate.ToString("p",ci)); // 84,00% ci.NumberFormat.PercentDecimalDigits = 1; Console.Write(passRate.ToString("p",ci)); // 84,0% }
In summary, .NET offers a variety of standard patterns that satisfy most needs to format dates and numbers. Behind the scenes, there are two classes, NumberFormatInfo and DateTimeFormatInfo, that define the symbols and rules used for formatting. .NET provides each culture with its own set of properties associated with an instance of these classes.
231
232
Chapter 5
5.7
■
C# Text Manipulation and File I/O
Regular Expressions
The use of strings and expressions to perform pattern matching dates from the earliest programming languages. In the mid-1960s SNOBOL was designed for the express purpose of text and string manipulation. It influenced the subsequent development of the grep tool in the Unix environment that makes extensive use of regular expressions. Those who have worked with grep or Perl or other scripting languages will recognize the similarity in the .NET implementation of regular expressions. Pattern matching is based on the simple concept of applying a special pattern string to some text source in order to match an instance or instances of that pattern within the text. The pattern applied against the text is referred to as a regular expression, or regex, for short. Entire books have been devoted to the topic of regular expressions. This section is intended to provide the essential knowledge required to get you started using regular expressions in the .NET world. The focus is on using the Regex class, and creating regular expressions from the set of characters and symbols available for that purpose.
The Regex Class You can think of the Regex class as the engine that evaluates regular expressions and applies them to target strings. It provides both static and instance methods that use regexes for text searching, extraction, and replacement. The Regex class and all related classes are found in the System.Text.RegularExpressions namespace. Syntax: Regex( string pattern ) Regex( string pattern, RegexOptions)
Parameters: pattern
Regular expression used for pattern matching.
RegexOptions
An enum whose values control how the regex is applied. Values include: CultureInvariant—Ignore culture. IgnoreCase—Ignore upper- or lowercase. RightToLeft—Process string right to left.
Example: Regex r1 = new Regex(" "); // Regular expression is a blank String words[] = r1.Split("red blue orange yellow"); // Regular expression matches upper- or lowercase "at" Regex r2 = new Regex("at", RegexOptions.IgnoreCase);
5.7 Regular Expressions
As the example shows, creating a Regex object is quite simple. The first parameter to its constructor is a regular expression. The optional second parameter is one or more (separated by |) RegexOptions enum values that control how the regex is applied.
Regex Methods The Regex class contains a number of methods for pattern matching and text manipulation. These include IsMatch, Replace, Split, Match, and Matches. All have instance and static overloads that are similar, but not identical.
Core Recommendation If you plan to use a regular expression repeatedly, it is more efficient to create a Regex object. When the object is created, it compiles the expression into a form that can be used as long as the object exists. In contrast, static methods recompile the expression each time they are used.
Let’s now examine some of the more important Regex methods. We’ll keep the regular expressions simple for now because the emphasis at this stage is on understanding the methods—not regular expressions.
IsMatch() This method matches the regular expression against an input string and returns a boolean value indicating whether a match is found. string searchStr = "He went that a way"; Regex myRegex = new Regex("at"); // instance methods bool match = myRegex.IsMatch(searchStr); // true // Begin search at position 12 in the string match = myRegex.IsMatch(searchStr,12); // false // Static Methods – both return true match = Regex.IsMatch(searchStr,"at"); match = Regex.IsMatch(searchStr,"AT",RegexOptions.IgnoreCase);
Replace() This method returns a string that replaces occurrences of a matched pattern with a specified replacement string. This method has several overloads that permit you to specify a start position for the search or control how many replacements are made.
233
234
Chapter 5
■
C# Text Manipulation and File I/O
Syntax: static Replace (string input, string pattern, string replacement [,RegexOptions]) Replace(string input, string replacement) Replace(string input, string replacement, int count) Replace(string input, string replacement, int count, int startat)
The count parameter denotes the maximum number of matches; startat indicates where in the string to begin the matching process. There are also versions of this method—which you may want to explore further—that accept a MatchEvaluator delegate parameter. This delegate is called each time a match is found and can be used to customize the replacement process. Here is a code segment that illustrates the static and instance forms of the method: string newStr; newStr = Regex.Replace("soft rose","o","i"); // sift rise // instance method Regex myRegex = new Regex("o"); // regex = "o" // Now specify that only one replacement may occur newStr = myRegex.Replace("soft rose","i",1); // sift rose
Split() This method splits a string at each point a match occurs and places that matching occurrence in an array. It is similar to the String.Split method, except that the match is based on a regular expression rather than a character or character string. Syntax: String[] Split(string input) String[] Split(string input, int count) String[] Split(string input, int count, int startat) Static String[] Split(string input, string pattern)
Parameters: input
The string to split.
count
The maximum number of array elements to return. A count value of 0 results in as many matches as possible. If the number of matches is greater than count, the last match consists of the remainder of the string.
startat
The character position in input where the search begins.
pattern
The regex pattern to be matched against the input string.
5.7 Regular Expressions
This short example parses a string consisting of a list of artists’ last names and places them in an array. A comma followed by zero or more blanks separates the names. The regular expression to match this delimiter string is: ",[ ]*". You will see how to construct this later in the section. string impressionists = "Manet,Monet, Degas, Pissarro,Sisley"; // Regex to match a comma followed by 0 or more spaces string patt = @",[ ]*"; // Static method string[] artists = Regex.Split(impressionists, patt); // Instance method is used to accept maximum of four matches Regex myRegex = new Regex(patt); string[] artists4 = myRegex.Split(impressionists, 4); foreach (string master in artists4) Console.Write(master); // Output --> "Manet" "Monet" "Degas" "Pissarro,Sisley"
Match() and Matches() These related methods search an input string for a match to the regular expression. Match() returns a single Match object and Matches() returns the object MatchCollection, a collection of all matches. Syntax: Match Match(string input) Match Match(string input, int startat) Match Match(string input, int startat, int numchars) static Match(string input, string pattern, [RegexOptions])
The Matches method has similar overloads but returns a MatchCollection object. Match and Matches are the most useful Regex methods. The Match object they return is rich in properties that expose the matched string, its length, and its location within the target string. It also includes a Groups property that allows the matched string to be further broken down into matching substrings. Table 5-7 shows selected members of the Match class. The following code demonstrates the use of these class members. Note that the dot (.) in the regular expression functions as a wildcard character that matches any single character. string verse = "In Xanadu did Kubla Khan"; string patt = ".an..."; // "." matches any character Match verseMatch = Regex.Match(verse, patt); Console.WriteLine(verseMatch.Value); // Xanadu
235
236
Chapter 5
■
C# Text Manipulation and File I/O
Console.WriteLine(verseMatch.Index); // 3 // string newPatt = "K(..)"; //contains group(..) Match kMatch = Regex.Match(verse, newPatt); while (kMatch.Success) { Console.Write(kMatch.Value); // -->Kub -->Kha Console.Write(kMatch.Groups[1]); // -->ub -->ha kMatch = kMatch.NextMatch(); }
This example uses NextMatch to iterate through the target string and assign each match to kMatch (if NextMatch is left out, an infinite loop results). The parentheses surrounding the two dots in newPatt break the pattern into groups without affecting the actual pattern matching. In this example, the two characters after K are assigned to group objects that are accessed in the Groups collection. Table 5-7 Selected Members of the Match Class Member
Description
Index
Property returning the position in the string where the first character of the match is found.
Groups
A collection of groups within the class. Groups are created by placing sections of the regex with parentheses. The text that matches the pattern in parentheses is placed in the Groups collection.
Length
Length of the matched string.
Success
True or False depending on whether a match was found.
Value
Returns the matching substring.
NextMatch()
Returns a new Match with the results from the next match operation, beginning with the character after the previous match, if any.
Sometimes, an application may need to collect all of the matches before processing them—which is the purpose of the MatchCollection class. This class is just a container for holding Match objects and is created using the Regex.Matches method discussed earlier. Its most useful properties are Count, which returns the number of captures, and Item, which returns an individual member of the collection. Here is how the NextMatch loop in the previous example could be rewritten: string verse = "In Xanadu did Kubla Khan"; String newpatt = "K(..)"; foreach (Match kMatch in Regex.Matches(verse, newpatt))
5.7 Regular Expressions
Console.Write(kMatch.Value); // -->Kub -->Kha // Could also create explicit collection and work with it. MatchCollection mc = Regex.Matches(verse, newpatt); Console.WriteLine(mc.Count); // 2
Creating Regular Expressions The examples used to illustrate the Regex methods have employed only rudimentary regular expressions. Now, let’s explore how to create regular expressions that are genuinely useful. If you are new to the subject, you will discover that designing Regex patterns tends to be a trial-and-error process; and the endeavor can yield a solution of simple elegance—or maddening complexity. Fortunately, almost all of the commonly used patterns can be found on one of the Web sites that maintain a searchable library of Regex patterns (www.regexlib.com is one such site). A regular expression can be broken down into four different types of metacharacters that have their own role in the matching process: • •
•
•
Matching characters. These match a specific type of character—for example, \d matches any digit from 0 to 9. Repetition characters. Used to prevent having to repeat a matching character or item—for example, \d{3}can be used instead of \d\d\d to match three digits. Positional characters. Designate the location in the target string where a match must occur—for example, ^\d{3} requires that the match occur at the beginning of the string. Escape sequences. Use the backslash (\) in front of characters that otherwise have special meaning—for example, \} permits the right brace to be matched.
Table 5-8 summarizes the most frequently used patterns. Table 5-8 Regular Expression Patterns Pattern
Matching Criterion
Example
+
Match one or more occurrences of the previous item.
to+ matches too and tooo. It
*
Match zero or more occurrences of the previous item.
to* matches t or too or tooo.
?
Match zero or one occurrence of the previous item. Performs “non-greedy” matching.
te?n matches ten or tn. It
does not match t.
does not match teen.
237
238
Chapter 5
■
C# Text Manipulation and File I/O
Table 5-8 Regular Expression Patterns (continued) Pattern
Matching Criterion
Example
{n}
Match exactly n occurrences of the previous character.
te{2}n matches teen. It does not match ten or teeen.
{n,}
Match at least n occurrences of the previous character.
te{1,}n matches ten and
Match at least n and no more than m occurrences of the previous character.
{n,m}
teen. It does not match tn. te{1,2}n matches ten and
teen.
Treat the next character literally. Used to A\+B matches A+B. The slash match characters that have special (\) is required because + has meaning such as the patterns +, *, and ?. special meaning.
\
\d
\D
Match any digit (\d) or non-digit (\D). This is equivalent to [0-9] or [^0-9], respectively.
\d\d matches 55. \D\D matches xx.
\w
\W
Match any word plus underscore (\w) or non-word (\W) character. \w is equivalent to [a-zA-Z0-9_]. \W is equivalent to [^a-zA-Z0-9_] .
\w\w\w\w matches A_19 . \W\W\W matches ($).
\n \v
\r \f
Match newline, carriage return, tab, vertical tab, or form feed, respectively.
N/A
\s
\S
Match any whitespace (\s) or non-whitespace (\S). A whitespace is usually a space or tab character.
\w\s\w\s\w matches A B C.
Matches any single character. Does not match a newline.
a.c matches abc.
|
Logical OR.
"in|en" matches enquiry.
[. . . ]
Match any single character between the brackets. Hyphens may be used to indicate a range.
[aeiou] matches u. [\d\D]
All characters except those in the brackets.
[^aeiou] matches x.
. (dot)
[^. . .]
\t
It does not match abcc.
matches a single digit or non-digit.
5.7 Regular Expressions
A Pattern Matching Example Let’s apply these character patterns to create a regular expression that matches a Social Security Number (SSN): bool iMatch = Regex.IsMatch("245-09-8444", @"\d\d\d-\d\d-\d\d\d\d");
This is the most straightforward approach: Each character in the Social Security Number matches a corresponding pattern in the regular expression. It’s easy to see, however, that simply repeating symbols can become unwieldy if a long string is to be matched. Repetition characters improve this: bool iMatch = Regex.IsMatch("245-09-8444", @"\d{3}-\d{2}-\d{4}");
Another consideration in matching the Social Security Number may be to restrict where it exists in the text. You may want to ensure it is on a line by itself, or at the beginning or end of a line. This requires using position characters at the beginning or end of the matching sequence. Let’s alter the pattern so that it matches only if the Social Security Number exists by itself on the line. To do this, we need two characters: one to ensure the match is at the beginning of the line, and one to ensure that it is also at the end. According to Table 5-9, ^ and $ can be placed around the expression to meet these criteria. The new string is @"^\d{3}-\d{2}-\d{4}$"
These positional characters do not take up any space in the expression—that is, they indicate where matching may occur but are not involved in the actual matching process. Table 5-9 Characters That Specify Where a Match Must Occur Position Character
Description
^
Following pattern must be at the start of a string or line.
$
Preceding pattern must be at end of a string or line.
\A
Preceding pattern must be at the start of a string.
\b
\B
Move to a word boundary (\b), where a word character and non-word character meet, or a non-word boundary.
\z
\Z
Pattern must be at the end of a string (\z) or at the end of a string before a newline.
239
240
Chapter 5
■
C# Text Manipulation and File I/O
As a final refinement to the SSN pattern, let’s break it into groups so that the three sets of numbers separated by dashes can be easily examined. To create a group, place parentheses around the parts of the expression that you want to examine independently. Here is a simple code example that uses the revised pattern: string ssn = "245-09-8444"; string ssnPatt = @"^(\d{3})-(\d{2})-(\d{4})$"; Match ssnMatch = Regex.Match(ssn, ssnPatt); if (ssnMatch.Success){ Console.WriteLine(ssnMatch.Value); // 245-09-8444 Console.WriteLine(ssnMatch.Groups.Count); // 4 // Count is 4 since Groups[0] is set to entire SSN Console.Write(ssnMatch.Groups[1]); // 245 Console.Write(ssnMatch.Groups[2]); // 09 Console.Write(ssnMatch.Groups[3]); // 8444 }
We now have a useful pattern that incorporates position, repetition, and group characters. The approach that was used to create this pattern—started with an obvious pattern and refined it through multiple stages—is a useful way to create complex regular expressions (see Figure 5-4). Group 1 Group 2 Group 3 @"^(\d{3})-(\d{2})-(\d{4})$" match at end of string repeat \d 4 times match any single digit match from beginning of string Figure 5-4
Regular expression
Working with Groups As we saw in the preceding example, the text resulting from a match can be automatically partitioned into substrings or groups by enclosing sections of the regular expression in parentheses. The text that matches the enclosed pattern becomes a member of the Match.Groups[] collection. This collection can be indexed as a zero-based array: the 0 element is the entire match, element 1 is the first group, element 2 the second, and so on.
5.7 Regular Expressions
Groups can be named to make them easier to work with. The name designator is placed adjacent to the opening parenthesis using the syntax ?. To demonstrate the use of groups, let’s suppose we need to parse a string containing the forecasted temperatures for the week (for brevity, only two days are included): string txt ="Monday Hi:88 Lo:56 Tuesday Hi:91 Lo:61";
The regex to match this includes two groups: day and temps. The following code creates a collection of matches and then iterates through the collection, printing the content of each group: string rgPatt = @"(?[a-zA-Z]+)\s*(?Hi:\d+\s*Lo:\d+)"; MatchCollection mc = Regex.Matches(txt, rgPatt); //Get matches foreach(Match m in mc) { Console.WriteLine("{0} {1}", m.Groups["day"],m.Groups["temps"]); } //Output: Monday Hi:88 Lo:56 // Tuesday Hi:91 Lo:61
Core Note There are times when you do not want the presence of parentheses to designate a group that captures a match. A common example is the use of parentheses to create an OR expression—for example, (an|in|on). To make this a non-capturing group, place ?: inside the parentheses— for example, (?:an|in|on).
Backreferencing a Group It is often useful to create a regular expression that includes matching logic based on the results of previous matches within the expression. For example, during a grammatical check, word processors flag any word that is a repeat of the preceding word(s). We can create a regular expression to perform the same operation. The secret is to define a group that matches a word and then uses the matched value as part of the pattern. To illustrate, consider the following code: string speech = "Four score and and seven years"; patt = @"(\b[a-zA-Z]+\b)\s\1"; // Match repeated words MatchCollection mc = Regex.Matches(speech, patt);
241
242
Chapter 5
■
C# Text Manipulation and File I/O
foreach(Match m in mc) { Console.WriteLine(m.Groups[1]); }
// --> and
This code matches only the repeated words. Let’s examine the regular expression:
Text/Pattern
Description
and and @"(\b[a-zA-Z]+\b)\s
Matches a word bounded on each side by a word boundary (\b) and followed by a whitespace.
and and \1
The backreference indicator. Any group can be referenced with a slash (\) followed by the group number. The effect is to insert the group’s matched value into the expression.
A group can also be referenced by name rather than number. The syntax for this backreference is \k followed by the group name enclosed in : patt = @"(?\b[a-zA-Z]+\b)\s\k";
Examples of Using Regular Expressions This section closes with a quick look at some patterns that can be used to handle common pattern matching challenges. Two things should be clear from these examples: There are virtually unlimited ways to create expressions to solve a single problem, and many pattern matching problems involve nuances that are not immediately obvious.
Using Replace to Reverse Words string userName = "Claudel, Camille"; userName = Regex.Replace( userName, @"(\w+),\s*(\w+)", "$2 $1" ); Console.WriteLine(userName); // Camille Claudel
The regular expression assigns the last and first name to groups 1 and 2. The third parameter in the Replace method allows these groups to be referenced by placing $ in front of the group number. In this case, the effect is to replace the entire matched name with the match from group 2 (first name) followed by the match from group 1 (last name).
5.7 Regular Expressions
Parsing Numbers String myText = "98, 98.0, +98.0, +98"; string numPatt = @"\d+"; numPatt = @"(\d+\.?\d*)|(\.\d+)"; numPatt = @"([+-]?\d+\.?\d*)|([+-]?\.\d+)";
// Integer // Allow decimal // Allow + or -
Note the use of the OR (|) symbol in the third line of code to offer alternate patterns. In this case, it permits an optional number before the decimal. The following code uses the ^ character to anchor the pattern to the beginning of the line. The regular expression contains a group that matches four bytes at a time. The * character causes the group to be repeated until there is nothing to match. Each time the group is applied, it captures a 4-digit hex number that is placed in the CaptureCollection object. string hex = "00AA001CFF0C"; string hexPatt = @"^(?[a-fA-F\d]{4})*"; Match hexMatch = Regex.Match(hex,hexPatt); Console.WriteLine(hexMatch.Value); // --> 00AA001CFFOC CaptureCollection cc = hexMatch.Groups["hex4"].Captures; foreach (Capture c in cc) Console.Write(c.Value); // --> 00AA 001C FF0C
Figure 5-5 shows the hierarchical relationship among the Match, GroupCollection, and CaptureCollection classes.
Group name
Regex:
@"^(?[a-fA-F\d]{4})*"
Match = 00AA001CFF0C GroupCollection Group[0] Group[hex4] CaptureCollection 00AA 001C FF0C Figure 5-5
Hex numbers captured by regular expression
243
244
Chapter 5
5.8
■
C# Text Manipulation and File I/O
System.IO: Classes to Read and Write Streams of Data
The System.IO namespace contains the primary classes used to move and process streams of data. The data source may be in the form of text strings, as discussed in this chapter, or raw bytes of data coming from a network or device on an I/O port. Classes derived from the Stream class work with raw bytes; those derived from the TextReader and TextWriter classes operate with characters and text strings (see Figure 5-6). We’ll begin the discussion with the Stream class and look at how its derived classes are used to manipulate byte streams of data. Then, we’ll examine how data in a more structured text format is handled using the TextReader and TextWriter classes.
System.Object
Stream
TextWriter
Buffered Stream
StreamWriter
FileStream
StringWriter
MemoryStream Compression.GZipStream
TextReader
StreamReader StringReader
Figure 5-6 Selected System.IO classes
The Stream Class This class defines the generic members for working with raw byte streams. Its purpose is to abstract data into a stream of bytes independent of any underlying data devices. This frees the programmer to focus on the data stream rather than device characteristics. The class members support three fundamental areas of operation: reading, writing, and seeking (identifying the current byte position within a stream).
5.8 System.IO: Classes to Read and Write Streams of Data
Table 5-10 summarizes some of its important members. Not included are methods for asynchronous I/O, a topic covered in Chapter 13, “Asynchronous Programming and Multithreading.” Table 5-10
Selected Stream Members
Member
Description
CanRead CanSeek CanWrite
Indicates whether the stream supports reading, seeking, or writing.
Length
Length of stream in bytes; returns long type.
Position
Gets or sets the position within the current stream; has long type.
Close()
Closes the current stream and releases resources associated with it.
Flush()
Flushes data in buffers to the underlying device—for example, a file.
Read(byte array, offset, count) ReadByte()
Reads a sequence of bytes from the stream and advances the position within the stream to the number of bytes read. ReadByte reads one byte. Read returns number of bytes read; ReadByte returns –1 if at end of the stream.
SetLength()
Sets the length of the current stream. It can be used to extend or truncate a stream.
Seek()
Sets the position within the current stream.
Write(byte array, offset, count) WriteByte()
Writes a sequence of bytes (Write) or one byte (WriteByte) to the current stream. Neither has a return value.
These methods and properties provide the bulk of the functionality for the FileStream, MemoryStream, and BufferedStream classes, which we examine next.
FileStreams A FileStream object is created to process a stream of bytes associated with a backing store—a term used to refer to any storage medium such as disk or memory. The following code segment demonstrates how it is used for reading and writing bytes:
245
246
Chapter 5
■
C# Text Manipulation and File I/O
try { // Create FileStream object FileStream fs = new FileStream(@"c:\artists\log.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite); byte[] alpha = new byte[6] {65,66,67,68,69,70}; //ABCDEF // Write array of bytes to a file // Equivalent to: fs.Write(alpha,0, alpha.Length); foreach (byte b in alpha) { fs.WriteByte(b);} // Read bytes from file fs.Position = 0; // Move to beginning of file for (int i = 0; i< fs.Length; i++) Console.Write((char) fs.ReadByte()); //ABCDEF fs.Close(); catch(Exception ex) { Console.Write(ex.Message); }
As this example illustrates, a stream is essentially a byte array with an internal pointer that marks a current location in the stream. The ReadByte and WriteByte methods process stream bytes in sequence. The Position property moves the internal pointer to any position in the stream. By opening the FileStream for ReadWrite, the program can intermix reading and writing without closing the file.
Creating a FileStream The FileStream class has several constructors. The most useful ones accept the path of the file being associated with the object and optional parameters that define file mode, access rights, and sharing rights. The possible values for these parameters are shown in Figure 5-7. FileStream(file name, FileMode, FileAccess, FileShare) None Read Append Read ReadWrite Create ReadWrite CreateNew Write Write Open OpenOrCreate Truncate Default Figure 5-7
Options for FileStream constructors
5.8 System.IO: Classes to Read and Write Streams of Data
The FileMode enumeration designates how the operating system is to open the file and where to position the file pointer for subsequent reading or writing. Table 5-11 is worth noting because you will see the enumeration used by several classes in the System.IO namespace. Table 5-11 FileMode Enumeration Values Value
Description
Append
Opens an existing file or creates a new one. Writing begins at the end of the file.
Create
Creates a new file. An existing file is overwritten.
CreateNew
Creates a new file. An exception is thrown if the file already exists.
Open
Opens an existing file.
OpenOrCreate
Opens a file if it exists; otherwise, creates a new one.
Truncate
Opens an existing file, removes its contents, and positions the file pointer to the beginning of the file.
The FileAccess enumeration defines how the current FileStream may access the file; FileShare defines how file streams in other processes may access it. For example, FileShare.Read permits multiple file streams to be created that can simultaneously read the same file.
MemoryStreams As the name suggests, this class is used to stream bytes to and from memory as a substitute for a temporary external physical store. To demonstrate, here is an example that copies a file. It reads the original file into a memory stream and then writes this to a FileStream using the WriteTo method: FileStream fsIn = new FileStream(@"c:\manet.bmp", FileMode.Open, FileAccess.Read); FileStream fsOut = new FileStream(@"c:\manetcopy.bmp", FileMode.OpenOrCreate, FileAccess.Write); MemoryStream ms = new MemoryStream(); // Input image byte-by-byte and store in memory stream int imgByte; while ((imgByte = fsIn.ReadByte())!=-1){ ms.WriteByte((byte)imgByte); }
247
248
Chapter 5
■
C# Text Manipulation and File I/O
ms.WriteTo(fsOut); // Copy image from memory to disk byte[] imgArray = ms.ToArray(); // Convert to array of bytes fsIn.Close(); fsOut.Close(); ms.Close();
BufferedStreams One way to improve I/O performance is to limit the number of reads and writes to an external device—particularly when small amounts of data are involved. Buffers have long offered a solution for collecting small amounts of data into larger amounts that could then be sent more efficiently to a device. The BufferedStream object contains a buffer that performs this role for an underlying stream. You create the object by passing an existing stream object to its constructor. The BufferedStream then performs the I/O operations, and when the buffer is full or closed, its contents are flushed to the underlying stream. By default, the BufferedStream maintains a buffer size of 4096 bytes, but passing a size parameter to the constructor can change this. Buffers are commonly used to improve performance when reading bytes from an I/O port or network. Here is an example that associates a BufferedStream with an underlying FileStream. The heart of the code consists of a loop in which FillBytes (simulating an I/O device) is called to return an array of bytes. These bytes are written to a buffer rather than directly to the file. When fileBuffer is closed, any remaining bytes are flushed to the FileStream fsOut1. A write operation to the physical device then occurs. private void SaveStream() { Stream fsOut1 = new FileStream(@"c:\captured.txt", FileMode.OpenOrCreate, FileAccess.Write); BufferedStream fileBuffer = new BufferedStream(fsOut1); byte[] buff; // Array to hold bytes written to buffer bool readMore=true; while(readMore) { buff = FillBytes(); // Get array of bytes for (int j = 0;j0) (string.Compare("Alpha","alpha",true) ==0) (string.CompareOrdinal("Alpha","alpha")>0) (string.Equals("alpha","Alpha"))
265
BUILDING WINDOWS FORMS APPLICATIONS
Topics in This Chapter • Introduction: With just a few lines of code, you can build a Windows Forms (WinForms) application that demonstrates the basics of event handling and creating child forms. • Using Form Controls: All controls inherit from the base Control class. The members of this class provide a uniform way of positioning, sizing, and modifying a control’s appearance. • The Form Class: The Form class includes custom properties that affect its appearance, and enable it to work with menus and manage child forms. • Message and Dialog Boxes: A pop-up window to provide user interaction or information can be created as a message or dialog box. • MDI Forms: A Multiple Document Interface (MDI) is a container that holds child forms. A main menu permits the forms to be organized, accessed, and manipulated. • Working with Menus: .NET supports two types of menus: a main form menu and a context menu that can be associated with individual controls. • Adding Help to a Form: Help buttons, ToolTips, and HTML Help are options for adding help to a form. • Form Inheritance: Visual inheritance enables a form to be created quickly, by inheriting the interface elements and functionality from another form.
6
This chapter is aimed at developers responsible for creating Graphical User Interface (GUI) applications for the desktop—as opposed to applications that run on a Web server or mobile device. The distinction is important because .NET provides separate class libraries for each type of application and groups them into distinct namespaces: • • •
System.Windows.Forms. Windows Forms (WinForms). System.Web.UIWebControls. Web Forms. System.Web.UIMobileControls. Mobile Forms for hand-held and
pocket devices. Although this chapter focuses on Windows Forms, it is important to recognize that modern applications increasingly have to support multiple environments. Acknowledging this, .NET strives to present a uniform “look and feel” for applications developed in each. The forms and controls in all three namespaces provide similar, but not identical, functionality. The knowledge you acquire in this chapter will shorten the learning curve required to develop .NET applications for the Web and mobile devices. Developers usually rely on an Integrated Development Environment (IDE), such as Visual Studio, to develop GUI applications. This makes it easy to overlook the fact that a form is a class that inherits from other classes and has its own properties and methods. To provide a true understanding of forms, this chapter peers beneath the IDE surface at the class members and how their implementation defines and affects the behavior of the form. The discussion includes how to display forms, resize them, make them scrollable or transparent, create inherited forms, and have them react to mouse and keyboard actions. 267
268
Chapter 6
■
Building Windows Forms Applications
This is not a chapter about the principles of GUI design, but it does demonstrate how adding amenities, such as Help files and a tab order among controls, improves form usability. Controls, by the way, are discussed only in a generic sense. A detailed look at specific controls is left to Chapter 7, “Windows Forms Controls.”
6.1
Programming a Windows Form
All Windows Forms programs begin execution at a designated main window. This window is actually a Form object that inherits from the System.Windows. Forms.Form class. The initial window is displayed by passing an instance of it to the static Application.Run method. The challenge for the developer is to craft an interface that meets the basic rule of design—form follows function. This means that a form’s design should support its functionality to the greatest extent possible. To achieve this, a developer must understand the properties, methods, and events of the Form class, as well as those of the individual controls that are placed on the form.
Building a Windows Forms Application by Hand Let’s create a simple Windows application using a text editor and the C# compiler from the command line. This application, shown in Listing 6-1, consists of a single window with a button that pops up a message when the button is clicked. The simple exercise demonstrates how to create a form, add a control to it, and set up an event handler to respond to an event fired by the control.
Listing 6-1
Basic Windows Forms Application Built by Hand
using System; using System.Windows.Forms; using System.Drawing; class MyWinApp { static void Main() { // (1) Create form and invoke it Form mainForm = new SimpleForm(); Application.Run(mainForm); } }
6.1 Programming a Windows Form
Listing 6-1
Basic Windows Forms Application Built by Hand (continued)
// User Form derived from base class Form class SimpleForm:Form { private Button button1; public SimpleForm() { this.Text = "Hand Made Form"; // (2) Create a button control and set some attributes button1 = new Button(); button1.Location = new Point(96,112); button1.Size = new Size(72,24); button1.Text= "Status"; this.Controls.Add(button1); // (3) Create delegate to call routine when click occurs button1.Click += new EventHandler(button1_Click); } void button1_Click(object sender, EventArgs e) { MessageBox.Show("Up and Running"); } }
Recall from Chapter 1, “Introduction to .NET and C#,” that command-line compilation requires providing a target output file and a reference to any required assemblies. In this case, we include the System.Windows.Forms assembly that contains the necessary WinForms classes. To compile, save the preceding source code as winform.cs and enter the following statement at the command prompt: csc /t:winform.exe /r:System.Windows.Forms.dll winform.cs
After it compiles, run the program by typing winform; the screen shown in Figure 6-1 should appear. The output consists of a parent form and a second form created by clicking the button. An important point to note is that the parent form cannot be accessed as long as the second window is open. This is an example of a modal form, where only the last form opened can be accessed. The alternative is a modeless form, in which a parent window spawns a child window and the user can access either the parent or child window(s). Both of these are discussed later.
269
270
Chapter 6
■
Building Windows Forms Applications
Figure 6-1
Introductory Windows application
The code breaks logically into three sections: 1. Form Creation. The parent form is an instance of the class SimpleForm, which inherits from Form and defines the form’s custom features. The form—and program—is invoked by passing the instance to the Application.Run method. 2. Create Button Control. A control is placed on a form by creating an instance of the control and adding it to the form. Each form has a Controls property that returns a Control.Collection type that represents the collection of controls contained on a form. In this example, the Controls.Add method is used to add a button to the form. Note that a corresponding Remove method is also available to dynamically remove a control from its containing form. An IDE uses this same Add method when you drag a control onto a form at design time. However, if you want to add or delete controls at runtime, you will be responsible for the coding. Controls have a number of properties that govern their appearance. The two most basic ones are Size and Location. They are implemented as: button1.Size = new Size(72,24); // width, height button1.Location = new Point(96,112); //x,y
The struct Size has a constructor that takes the width and height as parameters; the constructor for Point accepts the x and y coordinates of the button within the container.
6.2 Windows.Forms Control Classes
3. Handle Button Click Event. Event handling requires providing a method to respond to the event and creating a delegate that invokes the method when the event occurs (refer to Chapter 3, “Class Design in C#,” for details of event handling). In this example, button1_Click is the method that processes the event. The delegate associated with the Click event is created with the following statement: button1.Click += new EventHandler(button1_Click);
This statement creates an instance of the built-in delegate EventHandler and registers the method button1_Click with it.
Core Note .NET 2.0 adds a feature known as Partial Types, which permits a class to be physically separated into different files. To create a partial class, place the keyword partial in front of class in each file containing a segment of the class. Note that only one class declaration should specify Forms inheritance. The compilation process combines all the files into one class—identical to a single physical class. For Windows applications, partial classes seem something of a solution in search of a problem. However, for Web applications, they serve a genuine need, as is discussed in Chapter 16, “ASP.NET Web Forms and Controls.”
This exercise should emphasize the fact that working with forms is like working with any other classes in .NET. It requires gaining a familiarity with the class members and using standard C# programming techniques to access them.
6.2
Windows.Forms Control Classes
The previous examples have demonstrated how a custom form is derived from the Windows.Forms.Form class. Now, let’s take a broader look at the class hierarchy from which the form derives and the functionality that each class offers (see Figure 6-2).
271
272
Chapter 6
■
Building Windows Forms Applications
Control DataGridView Label ListView PictureBox TreeView GroupBox
ScrollableControl Container Control Form User Form Figure 6-2
Windows Forms class hierarchy
The Control Class It may seem counterintuitive that the Form class is below the Control class on the hierarchical chain, because a form typically contains controls. But the hierarchy represents inheritance, not containment. A Form is a container control, and the generic members of the Control class can be applied to it just as they are to a simple Label or TextBox. System.Windows.Forms.dll contains more than fifty controls that are available for use on a Windows Form. A few that inherit directly from Control are listed in Figure 6-2. All controls share a core set of inherited properties, methods, and events. These members allow you to size and position the control; adorn it with text, colors, and style features; and respond to keyboard and mouse events. This chapter examines the properties that are common to all inheriting controls; Chapter 7 offers a detailed look at the individual controls and how to use their distinct features in applications.
Control Properties Table 6-1 defines some of the properties common to most types that inherit from Control. Table 6-1 Common Control Properties Category
Property
Description
Size and position
Size
A Size object that exposes the width and height.
Location
Location is a Point object that specifies the x and y coordinates of the top left of the control.
6.2 Windows.Forms Control Classes
Table 6-1 Common Control Properties (continued) Category
Property
Description
Size and position (continued)
Width, Height, Top, Left, Right
These int values are derived from the size and location of the object. For example, Right is equal to Left + Width; Bottom is equal to Top + Height.
Bounds
Bounds is a rectangle that defines a control’s position and size: Button.Bounds = new Rectangle (10,10,50,60)
Color and appearance
Text
ClientRectangle
ClientRectangle is a rectangle that represents the client area of the control—that is, the area excluding scroll bars, titles, and so on.
Anchor
Specifies which edges of the container the control is anchored to. This is useful when the container is resized.
Dock
Controls which edge of the container the control docks to.
BackColor, ForeColor
Specifies the background and foreground color for the control. Color is specified as a static property of the Color structure.
BackGroundImage
BackGroundImage specifies an image to be used in the background of the control.
Text
Text associated with the control.
Font
Font describes the characteristics of the text font:
typeface, size, bold, and so on. Focus
Keyboard and mouse
TabIndex
int value that indicates the tab order of this control within its container.
TabStop
Boolean value that indicates whether this control can receive focus from the Tab key.
Focused
Boolean value indicating whether a control has input focus.
Visible
Boolean value indicating whether the control is displayed.
MouseButtons
Returns the current state of the mouse buttons (left, right, and middle).
273
274
Chapter 6
■
Building Windows Forms Applications
Table 6-1 Common Control Properties (continued) Category
Property
Description
Keyboard and mouse (continued)
MousePosition
Returns a Point type that specifies the cursor position.
ModifierKeys
Indicates which of the modifier keys (Shift, Ctrl, Alt) is pressed.
Cursor
Specifies the shape of the mouse pointer when it is over the control. Assigned value is a static property of the Cursors class. These include: .Hand .Beam
Runtime status
.Cross .Arrow
UpArrow WaitCursor
Default
Handle
int value representing the handle of Windows control.
Focused
bool value indicating whether the control has focus.
Working with Controls When you drag a control onto a form, position it, and size it, VisualStudio.NET (VS.NET) automatically generates code that translates the visual design to the underlying property values. There are times, however, when a program needs to modify controls at runtime to hide them, move them, and even resize them. In fact, size and position are often based on the user’s screen size, which can only be detected at runtime. An IDE cannot do this, so it is necessary that a programmer understand how these properties are used to design an effective control layout.
Size and Position As we saw in the earlier example, the size of a control is determined by the Size object, which is a member of the System.Drawing namespace: button1.Size = new Size(80,40); button2.Size = button1.Size;
// (width, height) // Assign size to second button
A control can be resized during runtime by assigning a new Size object to it. This code snippet demonstrates how the Click event handler method can be used to change the size of the button when it is clicked: private void button1_Click(object sender, System.EventArgs e) { MessageBox.Show("Up and Running"); Button button1 = (Button) sender; //Cast object to Button button1.Size = new Size(90,20); //Dynamically resize
6.2 Windows.Forms Control Classes
The System.Drawing.Point object can be used to assign a control’s location. Its arguments set the x and y coordinates—in pixels—of the upper-left corner of a control. The x coordinate is the number of pixels from the left side of the container. The y coordinate is the number of pixels from the top of the container. button1.Location = new Point(20,40);
// (x,y) coordinates
It is important to recognize that this location is relative to the control’s container. Thus, if a button is inside a GroupBox, the button’s location is relative to it and not the form itself. A control’s Location also can be changed at runtime by assigning a new Point object. Another approach is to set the size and location in one statement using the Bounds property: button1.Bounds = new Rectangle(20,40, 100,80);
A Rectangle object is created with its upper-left corner at the x,y coordinates (20,40) and its lower-right coordinates at (100,80). This corresponds to a width of 80 pixels and a height of 40 pixels.
How to Anchor and Dock a Control The Dock property is used to attach a control to one of the edges of its container. By default, most controls have docking set to none; some exceptions are the StatusStrip/StatusBar that is set to Bottom and the ToolStrip/ToolBar that is set to Top. The options, which are members of the DockStyle enumeration, are Top, Bottom, Left, Right, and Fill. The Fill option attaches the control to all four corners and resizes it as the container is resized. To attach a TextBox to the top of a form, use TextBox1.Dock = DockStyle.Top;
Figure 6-3 illustrates how docking affects a control’s size and position as the form is resized. This example shows four text boxes docked, as indicated. Resizing the form does not affect the size of controls docked on the left or right. However, controls docked to the top and bottom are stretched or shrunk horizontally so that they take all the space available to them.
Core Note The Form class and all other container controls have a DockPadding property that can be set to control the amount of space (in pixels) between the container’s edge and the docked control.
275
276
Chapter 6
■
Building Windows Forms Applications
Figure 6-3
Control resizing and positioning using the Dock property
The Anchor property allows a control to be placed in a fixed position relative to a combination of the top, left, right, or bottom edge of its container. Figure 6-4 illustrates the effects of anchoring.
Figure 6-4
How anchoring affects the resizing and positioning of controls
The distance between the controls’ anchored edges remains unchanged as the form is stretched. The PictureBox (1) is stretched horizontally and vertically so that it remains the same distance from all edges; the Panel (2) control maintains a constant distance from the left and bottom edge; and the Label (3), which is anchored only to the top, retains its distance from the top, left, and right edges of the form. The code to define a control’s anchor position sets the Anchor property to values of the AnchorStyles enumeration (Bottom, Left, None, Right, Top). Multiple values are combined using the OR ( | ) operator: btnPanel.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left);
6.2 Windows.Forms Control Classes
Tab Order and Focus Tab order defines the sequence in which the input focus is given to controls when the Tab key is pressed. The default sequence is the order in which the controls are added to the container. The tab order should anticipate the logical sequence in which users expect to input data and thus guide them through the process. The form in Figure 6-5 represents such a design: The user can tab from the first field down to subsequent fields and finally to the button that invokes the final action.
Figure 6-5 Tab order for controls on a form
Observe two things in the figure: First, even though labels have a tab order, they are ignored and never gain focus; and second, controls in a container have a tab order that is relative to the container—not the form. A control’s tab order is determined by the value of its TabIndex property: TextBox1.TabIndex = 0;
//First item in tab sequence
In VS.NET, you can set this property directly with the Property Manager, or by selecting ViewTabOrder and clicking the boxes over each control to set the value. If you do not want a control to be included in the tab order, set its TabStop value to false. This does not, however, prevent it from receiving focus from a mouse click. When a form is loaded, the input focus is on the control (that accepts mouse or keyboard input) with the lowest TabIndex value. During execution, the focus can be given to a selected control using the Focus method: if(textBox1.CanFocus) { textBox1.Focus(); }
277
278
Chapter 6
■
Building Windows Forms Applications
Iterating Through Controls on a Form All controls on a form are contained in a Controls collection. By enumerating through this collection, it is possible to examine each control, identify it by name and type, and modify properties as needed. One common use is to clear the value of selected fields on a form in a refresh operation. This short example examines each control in Figure 6-5 and displays its name and type: int ctrlCt = this.Controls.Count; // 8 foreach (Control ct in this.Controls) { object ob = ct.GetType(); MessageBox.Show(ob.ToString()); //Displays type as string MessageBox.Show(ct.Name); }
There are several things to be aware of when enumerating control objects: •
•
•
The type of each control is returned as a fully qualified name. For example, a TextBox is referred to as System.Forms.Form.TextBox. Only a container’s top-level objects are listed. In this example, the Controls.Count value is 8 rather than 10 because the GroupBox is counted as one control and its child controls are excluded. You can use a control’s HasChildren property to determine if it is a container. Listing 6-2 uses recursion to list all child controls.
Listing 6-2
Enumerate All Controls on a Form Recursively
void IterateThroughControls(Control parent) { foreach (Control c in parent.Controls) { MessageBox.Show(c.ToString()); if(c.HasChildren) { IterateThroughControls(c); } } }
Applying this code to Figure 6-5 results in all controls on the main form being listed hierarchically. A control is listed followed by any child it may have.
6.2 Windows.Forms Control Classes
Control Events When you push a key on the keyboard or click the mouse, the control that is the target of this action fires an event to indicate the specific action that has occurred. A registered event handling routine then responds to the event and formulates what action to take. The first step in handling an event is to identify the delegate associated with the event. You must then register the event handling method with it, and make sure the method’s signature matches the parameters specified by the delegate. Table 6-2 summarizes the information required to work with mouse and keyboard triggered events. Table 6-2 Control Events
Event
Built-In Delegate/ Parameters
Description
Click, DoubleClick, MouseEnter, MouseLeave, MouseHover, MouseWheel
EventHandler ( object sender, EventArgs e)
Events triggered by clicking, double clicking, or moving the mouse.
MouseDown, MouseUp, MouseMove
MouseEventHandler ( object sender, MouseEventArgs)
Events triggered by mouse and mouse button motions. Note that this event is not triggered if the mouse action occurs within a control in the current container.
KeyUp, KeyDown
KeyEventHandler ( object sender, KeyEventArgs e)
Events triggered by key being raised or lowered.
KeyPress
KeyPressEventHandler ( object sender, KeyPressEventArgs e)
Event triggered by pressing any key.
Handling Mouse Events In addition to the familiar Click and DoubleClick events, all Windows Forms controls inherit the MouseHover, MouseEnter, and MouseLeave events. The latter two are fired when the mouse enters and leaves the confines of a control. They are useful for creating a MouseOver effect that is so common to Web pages.
279
280
Chapter 6
■
Building Windows Forms Applications
To illustrate this, let’s consider an example that changes the background color on a text box when a mouse passes over it. The following code sets up delegates to call OnMouseEnter and OnMouseLeave to perform the background coloring: TextBox userID = new TextBox(); userID.MouseEnter += new EventHandler(OnMouseEnter); userID.MouseLeave += new EventHandler(OnMouseLeave);
The event handler methods match the signature of the EventHandler delegate and cast the sender parameter to a Control type to access its properties. private void OnMouseEnter(object sender, System.EventArgs e){ Control ctrl = (Control) sender; ctrl.BackColor= Color.Bisque; } private void OnMouseLeave(object sender, System.EventArgs e){ Control ctrl = (Control) sender; ctrl.BackColor= Color.White; }
Core Note It is possible to handle events by overriding the default event handlers of the base Control class. These methods are named using the pattern Oneventname—for example, OnMouseDown, OnMouseMove, and so on. To be consistent with .NET, the delegate approach is recommended over this. It permits a control to specify multiple event handlers for an event, and it also permits a single event handler to process multiple events.
The delegates for the MouseDown, MouseUp, and MouseMove events take a second argument of the MouseEventArgs type rather than the generic EventArgs type. This type reveals additional status information about a mouse via the properties shown in Table 6-3. The properties in this table are particularly useful for applications that must track mouse movement across a form. Prime examples are graphics programs that rely on mouse location and button selection to control onscreen drawing. To illustrate, Listing 6-3 is a simple drawing program that draws a line on a form’s surface, beginning at the point where the mouse key is pressed and ending where it is raised. To make it a bit more interesting, the application draws the line in black if the left button is dragged, and in red if the right button is dragged.
6.2 Windows.Forms Control Classes
Table 6-3 Properties of MouseEventArgs Property
Description
Button
Indicates which mouse button was pressed. The attribute value is defined by the MouseButtons enumeration: Left, Middle, None, Right
Clicks
Number of clicks since last event.
Delta
Number of detents the mouse wheel rotates. A positive number means it’s moving forward; a negative number shows backward motion.
X, Y
Mouse coordinates relative to the container’s upper-left corner. This is equivalent to the control’s MousePosition property.
Listing 6-3
Using Mouse Events to Draw on a Form
using System; using System.Windows.Forms; using System.Drawing; class MyWinApp { static void Main() { Form mainForm = new DrawingForm(); Application.Run(mainForm); } } // User Form derived from base class Form class DrawingForm:Form { Point lastPoint = Point.Empty; // Save coordinates public Pen myPen; //Defines color of line public DrawingForm() { this.Text = "Drawing Pad"; // reate delegates to call MouseUp and MouseDown this.MouseDown += new MouseEventHandler(OnMouseDown); this.MouseUp += new MouseEventHandler(OnMouseUp); }
281
282
Chapter 6
■
Listing 6-3
Building Windows Forms Applications
Using Mouse Events to Draw on a Form (continued)
private void OnMouseDown(object sender, MouseEventArgs e) { myPen = (e.Button==MouseButtons.Right)? Pens.Red: Pens.Black; lastPoint.X = e.X; lastPoint.Y = e.Y; } private void OnMouseUp(object sender, MouseEventArgs e) { // Create graphics object Graphics g = this.CreateGraphics(); if (lastPoint != Point.Empty) g.DrawLine(myPen, lastPoint.X,lastPoint.Y,e.X,e.Y); lastPoint.X = e.X; lastPoint.Y = e.Y; g.Dispose(); } }
Even without an understanding of .NET graphics, the role of the graphics-related classes should be self-evident. A Graphics object is created to do the actual drawing using its DrawLine method. The parameters for this method are the Pen object that defines the color and the coordinates of the line to be drawn. When a button is pressed, the program saves the coordinate and sets myPen based on which button is pressed: a red pen for the right button and black for the left. When the mouse button is raised, the line is drawn from the previous coordinate to the current location. The MouseEventArgs object provides all the information required to do this.
Handling Keyboard Events Keyboard events are also handled by defining a delegate to call a custom event handling method. Two arguments are passed to the event handler: the sender argument identifies the object that raised the event and the second argument contains fields describing the event. For the KeyPress event, this second argument is of the KeyPressEventArgs type. This type contains a Handled field that is set to true by the event handling routine to indicate it has processed the event. Its other property is KeyChar, which identifies the key that is pressed. KeyChar is useful for restricting the input that a field accepts. This code segment demonstrates how easy it is to limit a field’s input to digits. When a non-digit is entered, Handled is set to true, which prevents the form engine from displaying the character. Otherwise, the event handling routine does nothing and the character is displayed.
6.2 Windows.Forms Control Classes
private void OnKeyPress(object sender, KeyPressEventArgs e) { if (! char.IsDigit(e.KeyChar)) e.Handled = true; }
The KeyPress event is only fired for printable character keys; it ignores non-character keys such as Alt or Shift. To recognize all keystrokes, it is necessary to turn to the KeyDown and KeyUp events. Their event handlers receive a KeyEventArgs type parameter that identifies a single keystroke or combination of keystrokes. Table 6-4 lists the important properties provided by KeyEventArgs. Table 6-4 KeyEventArgs Properties Member
Description
Alt, Control, Shift
Boolean value that indicates whether the Alt, Control, or Shift key was pressed.
Handled
Boolean value that indicates whether an event was handled.
KeyCode
Returns the key code for the event. This code is of the Keys enumeration type.
KeyData
Returns the key data for the event. This is also of the Keys enumeration type, but differs from the KeyCode in that it recognizes multiple keys.
Modifiers
Indicates which combination of modifier keys (Alt, Ctrl, and Shift) was pressed.
A few things to note: •
• •
The Alt, Control, and Shift properties are simply shortcuts for comparing the KeyCode value with the Alt, Control, or Shift member of the Keys enumeration. KeyCode represents a single key value; KeyData contains a value for a single key or combination of keys pressed. The Keys enumeration is the secret to key recognition because its members represent all keys. If using Visual Studio.NET, Intellisense lists all of its members when the enum is used in a comparison; otherwise, you need to refer to online documentation for the exact member name because the names are not always what you expect. For example, digits are designated by D1, D2, and so on.
283
284
Chapter 6
■
Building Windows Forms Applications
The preceding code segment showed how to use KeyPress to ensure a user presses only number keys (0–9). However, it does not prevent one from pasting non-numeric data using the Ctrl-V key combination. A solution is to use the KeyDown event to detect this key sequence and set a flag notifying the KeyPress event handler to ignore the attempt to paste. In this example, two event handlers are registered to be called when a user attempts to enter data into a text box using the keyboard. KeyDown is invoked first, and it sets paste to true if the user is pushing the Ctrl-V key combination. The KeyPress event handler uses this flag to determine whether to accept the key strokes. private bool paste; //Register event handlers for TextBox t. //They should be registered in this order, //because the last registered is the //first executed t.KeyPress += new KeyPressEventHandler(OnKeyPress); t.KeyDown += new KeyEventHandler(OnKeyDown); private void OnKeyDown(object sender, KeyEventArgs e) { if (e.Modifiers == Keys.Control && e.KeyCode == Keys.V) { paste=true; //Flag indicating paste attempt string msg = string.Format("Modifier: {0} \nKeyCode: {1} \nKeyData: {2}", e.Modifiers.ToString(), e.KeyCode.ToString(), e.KeyData.ToString()); MessageBox.Show(msg); //Display property values } private void OnKeyPress(object sender, KeyPressEventArgs e) { if (paste==true) e.Handled = true; }
This program displays the following values for the selected KeyEventArgs properties when Ctrl-V is pressed: Modifier: KeyCode: KeyData:
Control V Control, V
6.3 The Form Class
6.3
The Form Class
The Form object inherits all the members of the Control class as well as the ScrollableControl class, which provides properties that enable scrolling. To this it adds a large number of properties that enable it to control its appearance, work with child forms, create modal forms, display menus, and interact with the desktop via tool and status bars. Table 6-5 shows a selected list of these properties. Table 6-5 Selected Form Properties Category
Property
Description
Appearance
FormBorderStyle
Gets or sets the border style of the form. It is defined by the FormBorderStyle enumeration: Fixed3D FixedSingle FixedDialog
Size and position
None Sizable
ControlBox
Boolean value that determines whether the menu icon in the left corner of a form and the close button on the upper right are shown.
MaximizeBox MinimizeBox
Boolean value that indicates whether these buttons are displayed on the form.
Opacity
Gets or sets the opacity of the form and all of its controls. The maximum value (least transparent) is 1.0. Does not work with Windows 95/98.
TransparencyKey
A color that represents transparent areas on the form. Any control or portion of the form that has a back color equal to this color is not displayed. Clicking this transparent area sends the event to any form below it.
AutoScale
Indicates whether the form adjusts its size to accommodate the size of the font used.
ClientSize
Size of the form excluding borders and the title bar.
DesktopLocation
A Point type that indicates where the form is located on the desktop window.
285
286
Chapter 6
■
Building Windows Forms Applications
Table 6-5 Selected Form Properties (continued) Category
Property
Description
Size and position (continued)
StartPosition
Specifies the initial position of a form. It takes a FormStartPosition enum value: CenterParent—Centered within bounds of
parent form. CenterScreen—Centered within the display. Manual—Use the DeskTopLocation value. Windows DefaultLocation—Windows sets
value.
Owner forms
MinimumSize MaximumSize
A Size object that designates the maximum and minimum size for the form. A value of (0,0) indicates no minimum or maximum.
ShowInTaskBar
Boolean value specifying whether application is represented in Windows task bar. Default is true.
TopLevel TopMost
Indicates whether to display the form as a TopLevel window or TopMost window. A top-level window has no parent; top-most form is always displayed on top of all other non-TopMost forms.
WindowState
Indicates how the form is displayed on startup. It takes a value from the FormWindowState enumeration: Maximized, Minimized, or Normal.
Owner
The form designated as the owner of the form.
OwnedForms
A Form array containing the forms owned by a form.
Setting a Form’s Appearance The four properties shown in Figure 6-6 control which buttons and icon are present on the top border of a form. The Icon property specifies the .ico file to be used as the icon in the left corner; the ControlBox value determines whether the icon and close button are displayed (true) or not displayed (false); similarly, the MaximizeBox and MinimizeBox determine whether their associated buttons appear. The purpose of these properties is to govern functionality more than appearance. For example, it often makes sense to suppress the minimize and maximize buttons on modal forms in order to prevent a user from maximizing the form and hiding the underlying parent form.
6.3 The Form Class
Figure 6-6
Properties to control what appears on the title bar
Form Opacity A form’s opacity property determines its level of transparency. Values ranges from 0 to 1.0, where anything less than 1.0 results in partial transparency that allows elements beneath the form to be viewed. Most forms work best with a value of 1.0, but adjusting opacity can be an effective way to display child or TopMost forms that hide an underlying form. A common approach is to set the opacity of such a form to 1.0 when it has focus, and reduce the opacity when it loses focus. This technique is often used with search windows that float on top of the document they are searching. Let’s look at how to set up a form that sets its opacity to 1 when it is active and to .8 when it is not the active form. To do this, we take advantage of the Deactivate and Activated events that are triggered when a form loses or gains focus. We first set up the delegates to call the event handling routines: this.Deactivate += new System.EventHandler(Form_Deactivate); this.Activated += new System.EventHandler(Form_Activate);
The code for the corresponding event handlers is trivial: void Form_Deactivate(object sender, EventArgs e) { this.Opacity= .8; } void Form_Activate(object sender, EventArgs e) { this.Opacity= 1; }
Form Transparency Opacity affects the transparency of an entire form. There is another property, TransparencyKey, which can be used to make only selected areas of a form totally
transparent. This property designates a pixel color that is rendered as transparent when the form is drawn. The effect is to create a hole in the form that makes any area below the form visible. In fact, if you click a transparent area, the event is recognized by the form below. The most popular use of transparency is to create non-rectangular forms. When used in conjunction with a border style of FormBorderStyle.None to remove the
287
288
Chapter 6
■
Building Windows Forms Applications
title bar, a form of just about any geometric shape can be created. The next example illustrates how to create and use the cross-shaped form in Figure 6-7.
Figure 6-7 Form using transparency to create irregular appearance
The only requirement in creating the shape of the form is to lay out the transparent areas in the same color as the transparency key color. Be certain to select a color that will not be used elsewhere on the form. A standard approach is to set the back color of the form to the transparency key color, and draw an image in a different color that it will appear as the visible form. To create the form in Figure 6-7, place Panel controls in each corner of a standard form and set their BackColor property to Color.Red. The form is created and displayed using this code: CustomForm myForm = new CustomForm(); myForm.TransparencyKey = Color.Red; myForm.FormBorderStyle= FormBorderStyle.None; myForm.Show();
This achieves the effect of making the panel areas transparent and removing the title bar. With the title bar gone, it is necessary to provide a way for the user to move the form. This is where the mouse event handling discussed earlier comes to the rescue. At the center of the form is a multiple arrow image displayed in a PictureBox that the user can click and use to drag the form. Listing 6-4 shows how the MouseDown, MouseUp, and MouseMove events are used to implement form movement.
6.3 The Form Class
Listing 6-4
Using Form Transparency to Create a Non-Rectangular Form
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; public class CustomForm : Form { private Point lastPoint = Point.Empty; //Save mousedown public CustomForm() { InitializeComponent(); // set up form //Associate mouse events with pictureBox pictureBox1.MouseDown += new MouseEventHandler( OnMouseDown); pictureBox1.MouseUp += new MouseEventHandler(OnMouseUp); pictureBox1.MouseMove += new MouseEventHandler( OnMouseMove); } private void OnMouseDown(object sender, MouseEventArgs e) { lastPoint.X = e.X; lastPoint.Y = e.Y; } private void OnMouseUp(object sender, MouseEventArgs e) { lastPoint = Point.Empty; } //Move the form in response to the mouse being dragged private void OnMouseMove(object sender, MouseEventArgs e) { if (lastPoint != Point.Empty) { //Move form in response to mouse movement int xInc = e.X - lastPoint.X; int yInc = e.Y - lastPoint.Y; this.Location = new Point(this.Left + xInc, this.Top+yInc); } }
289
290
Chapter 6
■
Listing 6-4
Building Windows Forms Applications
Using Form Transparency to Create a Non-Rectangular Form (continued)
// Close Window private void button1_Click(object sender, System.EventArgs e) { this.Close(); } }
The logic is straightforward. When the user clicks the PictureBox, the coordinates are recorded as lastPoint. As the user moves the mouse, the Location property of the form is adjusted to reflect the difference between the new coordinates and the original saved location. When the mouse button is raised, lastPoint is cleared. Note that a complete implementation should also include code to handle form resizing.
Setting Form Location and Size The initial location of a form is determined directly or indirectly by its StartPosition property. As described in Table 6-6, it takes its value from the FormStartPosition enumeration. These values allow it to be placed in the center of the screen, in the center of a parent form, at a Windows default location, or at an arbitrarily selected location. Manual offers the greatest flexibility because it allows the program to set the location. The initial location is normally set in the Form.Load event handler. This example loads the form 200 pixels to the right of the upper-left corner of the screen: private void opaque_Load(object sender, System.EventArgs e) { this.DesktopLocation = new Point(200,0); }
The form’s initial location can also be set by the form that creates and displays the form object: opaque opForm = new opaque(); opForm.Opacity = 1; opForm.TopMost = true; //Always display form on top opForm.StartPosition = FormStartPosition.Manual; opForm.DesktopLocation = new Point(10,10); opForm.Show();
6.3 The Form Class
This code creates an instance of the form opaque and sets its TopMost property so that the form is always displayed on top of other forms in the same application. The DeskTopLocation property sets the form’s initial location. For it to work, however, the StartPostion property must first be set to FormStartPosition.Manual.
Core Note The DesktopLocation property sets coordinates within a screen’s working area, which is the area not occupied by a task bar. The Location property of the Control class sets the coordinates relative to the upper-left edge of the control’s container.
A form’s size can be set using either its Size or ClientSize property. The latter is usually preferred because it specifies the workable area of the form—the area that excludes the title bar, scrollbars, and any edges. This property is set to an instance of a Size object: this.ClientSize = new System.Drawing.Size(208, 133);
It is often desirable to position or size a form relative to the primary (.NET supports multiple screens for an application) screen size. The screen size is available through the Screen.PrimaryScreen.WorkingArea property. This returns a rectangle that represents the size of the screen excluding task bars, docked toolbars, and docked windows. Here is an example that uses the screen size to set a form’s width and height: int w = Screen.PrimaryScreen.WorkingArea.Width; int h = Screen.PrimaryScreen.WorkingArea.Height; this.ClientSize = new Size(w/4,h/4);
After a form is active, you may want to control how it can be resized. The aptly named MinimumSize and MaximumSize properties take care of this. In the following example, the maximum form size is set to one-half the width and height of the working screen area: //w and h are the screen's width and height this.MaximumSize = new Size(w/2,h/2); this.MinimumSize = new Size(200, 150);
Setting both width and height to zero removes any size restrictions.
291
292
Chapter 6
■
Building Windows Forms Applications
Displaying Forms After a main form is up and running, it can create instances of new forms and display them in two ways: using the Form.ShowDialog method or the Form.Show method inherited from the Control class. Form.ShowDialog displays a form as a modal dialog box. When activated, this type of form does not relinquish control to other forms in the application until it is closed. Dialog boxes are discussed at the end of this section. Form.Show displays a modeless form, which means that the form has no relationship with the creating form, and the user is free to select the new or original form. If the creating form is not the main form, it can be closed without affecting the new form; closing the main form automatically closes all forms in an application.
The Life Cycle of a Modeless Form A form is subject to a finite number of activities during its lifetime: It is created, displayed, loses and gains focus, and finally is closed. Most of these activities are accompanied by one or more events that enable the program to perform necessary tasks associated with the event. Table 6-6 summarizes these actions and events. Table 6-6 The Life Cycle of a Modeless Form Action
Events Triggered
Form object created
Description The form’s constructor is called. In Visual Studio, the InitializeComponent method is called to initialize the form.
Form.Show()
Form.Load Form.Activated
The Load event is called first, followed by the Activated event.
Form activated
Form.Activated
This occurs when the user selects the form. This becomes an “active” form.
Form deactivated
Form.Deactivate
Form is deactivated when it loses focus.
Form closed
Form.Deactivate Form.Closing Form.Closed
Form is closed by executing Form.Close or clicking on the form’s close button.
Form displayed:
Let’s look at some of the code associated with these events.
6.3 The Form Class
Creating and Displaying a Form When one form creates another form, there are coding requirements on both sides. The created form must set up code in its constructor to perform initialization and create controls. In addition, delegates should be set up to call event handling routines. If using Visual Studio.NET, any user initialization code should be placed after the call to InitializeComponent. For the class that creates the new form object, the most obvious task is the creation and display of the object. A less obvious task may be to ensure that only one instance of the class is created because you may not want a new object popping up each time a button on the original form is clicked. One way to manage this is to take advantage of the Closed event that occurs when a created form is closed (another way, using OwnedForms, is discussed shortly). If the form has not been closed, a new instance is not created. The code that follows illustrates this. An EventHandler delegate is set up to notify a method when the new form, opForm, is closed. A flag controls what action occurs when the button to create or display the form is pushed. If an instance of the form does not exist, it is created and displayed; if it does exist, the Form.Activate method is used to give it focus. //Next statement is at beginning of form's code public opaque opForm; bool closed = true; //Flag to indicate if opForm exists //Create new form or give focus to existing one private void button1_Click(object sender, System.EventArgs e) { if (closed) { closed = false; opForm = new opaque(); //Call OnOpClose when new form closes opForm.Closed += new EventHandler(OnOpClose); opForm.Show(); //Display new form object } else { opForm.Activate(); //Give focus to form } } //Event handler called when child form is closed private void OnOpClose(object sender, System.EventArgs e) { closed = true; //Flag indicating form is closed }
293
294
Chapter 6
■
Building Windows Forms Applications
Form Activation and Deactivation A form becomes active when it is first shown or later, when the user clicks on it or moves to it using an Alt-Tab key to iterate through the task bar. This fires the form’s Activated event. Conversely, when the form loses focus—through closing or deselection—the Deactivate event occurs. In the next code segment, the Deactivate event handler changes the text on a button to Resume and disables the button; the Activated event handler re-enables the button. this.Deactivate += new System.EventHandler(Form_Deactivate); this.Activated += new System.EventHandler(Form_Activate); // void Form_Deactivate(object sender, EventArgs e) { button1.Enabled = false; button1.Text = "Resume"; } void Form_Activate(object sender, EventArgs e) { button1.Enabled = true; }
Closing a Form The Closing event occurs as a form is being closed and provides the last opportunity to perform some cleanup duties or prevent the form from closing. This event uses the CancelEventHandler delegate to invoke event handling methods. The delegate defines a CancelEventArgs parameter that contains a Cancel property, which is set to true to cancel the closing. In this example, the user is given a final prompt before the form closes: this.Closing += new CancelEventHandler(Form_Closing); void Form_Closing(object sender, CancelEventArgs e) { if(MessageBox.Show("Are you sure you want to Exit?", "", MessageBoxButtons.YesNo) == DialogResult.No) { //Cancel the closing of the form e.Cancel = true; } }
Forms Interaction—A Sample Application When multiple form objects have been created, there must be a way for one form to access the state and contents of controls on another form. It’s primarily a matter of setting the proper access modifiers to expose fields and properties on each form. To illustrate, let’s build an application that consists of two modeless forms (see Figure 6-8). The main form contains two controls: a Textbox that holds the document
6.3 The Form Class
being processed and a Search button that, when clicked, passes control to a search form. The search form has a Textbox that accepts text to be searched for in the main form’s document. By default, the search phrase is any highlighted text in the document; it can also be entered directly by the user.
Figure 6-8
Text search application using multiple forms
When the Find Next button is pushed, the application searches for the next occurrence of the search string in the main document. If an occurrence is located, it is highlighted. To make it more interesting, the form includes options to search forward or backward and perform a case-sensitive or case-insensitive search. The main challenge in developing this application is to determine how each form makes the content of its controls available to the other form. DocForm, the main form, must expose the contents of documentText so that the search form can search the text in it and highlight an occurrence of matching text. The search form, SearchForm, must expose the contents of txtSearch, the TextBox containing the search phrase, so that the main form can set it to the value of any highlighted text before passing control to the form. DocForm shares the contents of documentText through a text box field myText that is assigned the value of documentText when the form loads. Setting myText to public static enables the search form to access the text box properties by simply qualifying them with DocForm.myText.
295
296
Chapter 6
■
Building Windows Forms Applications
public static TextBox myText;
//Declare public variable
private void docForm_Load(object sender, System.EventArgs e) { myText = documentText; } SearchForm exposes the contents of txtSearch to other objects through a write-only string property. public String SearchPhrase { set { txtSearch.Text = value;} }
//Write Only
DocForm, as well as any object creating an instance of SearchForm, can set this property. Now let’s look at the remaining code details of the two forms.
Code for the Main Form When the button on DocForm is clicked, the application either creates a new instance of SearchForm or passes control to an existing instance. In both cases, it first checks its text box and passes any highlighted text (SelectedText) to the SearchForm object via its SearchPhrase property (see Listing 6-5). Techniques described in earlier examples are used to create the object and set up a delegate to notify the DocForm object when the search form object closes.
Listing 6-5
Method to Pass Control to Search Form Instance
private void btnSearch_Click(object sender, System.EventArgs e) { //Create instance of search form if it does not exist if (closed) { closed= false; searchForm = new SearchForm(); //Create instance searchForm.TopMost = true; searchForm.Closed += new EventHandler(onSearchClosed); searchForm.StartPosition = FormStartPosition.Manual; searchForm.DesktopLocation = new Point(this.Right-200, this.Top-20); searchForm.SearchPhrase = documentText.SelectedText; searchForm.Show();
6.3 The Form Class
Listing 6-5
Method to Pass Control to Search Form Instance (continued)
} else { searchForm.SearchPhrase = documentText.SelectedText; searchForm.Activate(); } } private void onSearchClosed(object sender, System.EventArgs e) { closed= true; }
Code for the Search Form Listing 6-6 displays the code executed when the Find Next button is clicked. The search for the next occurrence of the search string can proceed up the document using the LastIndexOf method or down the document using IndexOf. Logic is also included to ignore or recognize case sensitivity.
Listing 6-6
Search for Matching Text on Another Form
private void btnFind_Click(object sender, System.EventArgs e) { int ib; //Index to indicate position of match string myBuff = DocForm.myText.Text; //Text box contents string searchText= this.txtSearch.Text; //Search string int ln = searchText.Length; //Length of search string if (ln>0) { //Get current location of selected text int selStart = DocForm.myText.SelectionStart; if (selStart >= DocForm.myText.Text.Length) { ib = 0; } else { ib = selStart + ln; } if (!this.chkCase.Checked) //Case-insensitive search { searchText = searchText.ToUpper(); myBuff = myBuff.ToUpper(); }
297
298
Chapter 6
■
Listing 6-6
Building Windows Forms Applications
Search for Matching Text on Another Form (continued)
if (this.radDown.Checked)ib = myBuff.IndexOf(searchText,ib); if (this.radUp.Checked && ib>ln-1)ib = myBuff.LastIndexOf(searchText,ib-2,ib-1); if (ib >= 0) //Highlight text on main form { DocForm.myText.SelectionStart = ib; DocForm.myText.SelectionLength = txtSearch.Text.Length; } } }
Owner and Owned Forms When a form displays an instance of a modeless form, it does not by default create an explicit relationship between itself and the new form. The forms operate autonomously: They either can be closed (except for a main form, which causes all forms to be closed) or minimized without affecting the other; and the creator form has no easy way to distinguish among instances of the forms it has launched. Often, however, one form does have a dependency on the other. In the preceding example, the floating search window exists only as a companion to a document that it searches. Its relationship to the form that created it is referred to as an owner-owned relationship. In .NET, this can be more than just a logical relationship. A form has an Owner property that can be set to the instance of the form that “owns” it. After this relationship is formally established, the behavior of the owner and owned form(s) is linked. For example, the owned form is always visually on top of its owner form. This eliminates the need to make SearchForm a TopMost form in our preceding example. An owner-owned relationship is easily established by setting the Owner property of a newly created form to the form that is creating it. opaque opForm = new opaque(); opForm.Owner = this; //Current form now owns new form opForm.Show();
This relationship affects the user’s interaction with the form in three ways: The owned form is always on top of the owner form even if the owner is active; closing the owner form also closes the owned form; and minimizing the owner form minimizes all owned forms and results in only one icon in the task bar.
6.3 The Form Class
Another advantage of the owner-owned relationship is that an owner form has an OwnedForms collection that contains all the owned forms it creates. The following example demonstrates how an owner form creates two owned forms, opForm and opForm2, and then enumerates the collection to set the caption of each form before
displaying it: opaque opForm = new opaque(); opForm.Owner = this; //Set current form to owner form opaque opForm2 = new opaque(); opForm2.Owner = this; //Set current form to owner form for (int ndx=0; ndx Abstract class Superseded by new control
*
Control
Label > > Button ComboBox CheckBox ListBox RadioButton CheckedListbox DataGridView ListView
DataGrid
*
Splitter
> TabControl TextBox ScrollableControl RichTextBox Panel GroupBox Flow/Table LayoutPanel PictureBox ToolStrip StatusBar
*
ToolBar
*
MenuStrip StatusStrip
TreeView
Figure 7-1
Windows Forms control hierarchy
Figure 7-1 shows the inheritance hierarchy of the Windows Forms controls. The controls marked by an asterisk (*) exist primarily to provide backward compatibility between .NET 2.0 and .NET 1.x. Specifically, the DataGrid has been superseded by the DataGridView, the StatusBar by the StatusStrip, and the ToolBar by the ToolStrip. Table 7-1 provides a summary of the more frequently used controls in this hierarchy.
7.1 A Survey of .NET Windows Forms Controls
Table 7-1 Selected Windows Forms Controls Control
Use
Button
Fires an event when a mouse click occurs or the Enter or Esc key is pressed.
CheckBox
Permits a user to select one or more options.
Description Represents a button on a form. Its text property determines the caption
displayed on the button’s surface. Consists of a check box with text or an image beside it. The check box can also be represented as a button by setting: checkBox1.Appearance = Appearance.Button
CheckedListBox
Displays list of items.
ListBox with checkbox preceding
each item in list. ComboBox
Provides TextBox and ListBox functionality.
Hybrid control that consists of a textbox and a drop-down list. It combines properties from both the TextBox and the ListBox.
DataGridView GridView
Manipulates data in a grid format.
The DataGridView is the foremost control to represent relational data. It supports binding to a database. The DataGridView was introduced in .NET 2.0 and supersedes the DataGrid.
GroupBox
Groups controls.
Use primarily to group radio buttons; it places a border around the controls it contains.
ImageList
Manages a collection of images.
Container control that holds a collection of images used by other controls such as the ToolStrip, ListView, and TreeView.
Label
Adds descriptive information to a form.
Text that describes the contents of a control or instructions for using a control or form.
ListBox
Displays a list of items— one or more of which may be selected.
May contain simple text or objects. Its methods, properties, and events allow items to be selected, modified, added, and sorted.
321
322
Chapter 7
■
Windows Forms Controls
Table 7-1 Selected Windows Forms Controls (continued) Control
Use
Description
ListView
Displays items and subitems.
May take a grid format where each row represents a different item and subitems. It also permits items to be displayed as icons.
MenuStrip
Adds a menu to a form.
Provides a menu and submenu system for a form. It supersedes the MainMenu control.
Panel FlowPanelLayout TablePanelLayout
Groups controls.
A visible or invisible container that groups controls. Can be made scrollable. FlowPanelLayout automatically aligns controls vertically or horizontally. TablePanelLayout aligns controls in
a grid. PictureBox
Contains a graphic.
Used to hold images in a variety of standard formats. Properties enable images to be positioned and sized within control’s borders.
ProgressBar
Depicts an application’s progress.
Displays the familiar progress bar that gives a user feedback regarding the progress of some event such as file copying.
RadioButton
Permits user to make one choice among a group of options.
Represents a Windows radio button.
StatusStrip
Provides a set of panels that indicate program status.
Provides a status bar that is used to provide contextual status information about current form activities.
TextBox
Accepts user input.
Can be designed to accept single- or multi-line input. Properties allow it to mask input for passwords, scroll, set letter casing automatically, and limit contents to read-only.
TreeView
Displays data as nodes in a tree.
Features include the ability to collapse or expand, add, remove, and copy nodes in a tree.
7.2 Button Classes, Group Box, Panel, and Label
This chapter lacks the space to provide a detailed look at each control. Instead, it takes a selective approach that attempts to provide a flavor of the controls and features that most benefit the GUI developer. Notable omissions are the DataGridView control, which is included in the discussion of data binding in Chapter 12, “Data Binding with Windows Forms Controls,” and the menu controls that were discussed in Chapter 6, “Building Windows Forms Applications.”
7.2
Button Classes, Group Box, Panel, and Label
The Button Class A button is the most popular way to enable a user to initiate some program action. Typically, the button responds to a mouse click or keystroke by firing a Click event that is handled by an event handler method that implements the desired response. constructor: public Button()
The constructor creates a button instance with no label. The button’s Text property sets its caption and can be used to define an access key (see Handling Button Events section); its Image property is used to place an image on the button’s background.
Setting a Button’s Appearance Button styles in .NET are limited to placing text and an image on a button, making it flat or three-dimensional, and setting the background/foreground color to any available color. The following properties are used to define the appearance of buttons, check boxes, and radio buttons: FlatStyle
This can take four values: FlatStyle.Flat, FlatStyle.Popup, FlatStyle.Standard, and FlatStyle.System. Standard is the usual three-dimensional button. Flat creates a flat button. Popup creates a flat button that becomes three-dimensional on a mouseover. System results in a button drawn to suit the style of the operating system.
Image
Specifies the image to be placed on the button. The Image.FromFile method is used to create the image object from a specified file: button1.Image = Image.FromFile("c:\\book.gif");
323
324
Chapter 7
■
ImageAlign
Windows Forms Controls
Specifies the position of the image on the button. It is set to a value of the ContentAlignment enum: button1.ImageAlign = ContentAlignment.MiddleRight;
TextAlign
Specifies the position of text on the image using the ContentAlignment value.
Handling Button Events A button’s Click event can be triggered in several ways: by a mouse click of the button, by pressing the Enter key or space bar, or by pressing the Alt key in combination with an access key. An access key is created by placing an & in front of one of the characters in the control’s Text property value. The following code segment declares a button, sets its access key to C, and registers an event handler to be called when the Click event is triggered: Button btnClose = new Button(); btnClose.Text= "&Close"; // Pushing ALT + C triggers event btnClose.Click += new EventHandler(btnClose_Clicked); // Handle Mouse Click, ENTER key, or Space Bar private void btnClose_Clicked(object sender, System.EventArgs e) { this.Close(); }
Note that a button’s Click event can also occur in cases when the button does not have focus. The AcceptButton and CancelButton form properties can specify a button whose Click event is triggered by pushing the Enter or Esc keys, respectively.
Core Suggestion Set a form’s CancelButton property to a button whose Click event handler closes the form. This provides a convenient way for users to close a window by pushing the Esc key.
The CheckBox Class The CheckBox control allows a user to select a combination of options on a form—in contrast to the RadioButton, which allows only one selection from a group. constructor: public CheckBox()
The constructor creates an unchecked check box with no label. The Text and Image properties allow the placement of an optional text description or image beside the box.
7.2 Button Classes, Group Box, Panel, and Label
Setting a CheckBox’s Appearance Check boxes can be displayed in two styles: as a traditional check box followed by text (or an image) or as a toggle button that is raised when unchecked and flat when checked. The appearance is selected by setting the Appearance property to Appearance.Normal or Appearance.Button. The following code creates the two check boxes shown in Figure 7-2. // Create traditional check box this.checkBox1 = new CheckBox(); this.checkBox1.Location = new System.Drawing.Point(10,120); this.checkBox1.Text = "La Traviata"; this.checkBox1.Checked = true; // Create Button style check box this.checkBox2 = new CheckBox(); this.checkBox2.Location = new System.Drawing.Point(10,150); this.checkBox2.Text = "Parsifal"; this.checkBox2.Appearance = Appearance.Button; this.checkBox2.Checked = true; this.checkBox2.TextAlign = ContentAlignment.MiddleCenter;
Figure 7-2 CheckBox styles
The RadioButton Class The RadioButton is a selection control that functions the same as a check box except that only one radio button within a group can be selected. A group consists of multiple controls located within the same immediate container. constructor: public RadioButton()
The constructor creates an unchecked RadioButton with no associated text. The Text and Image properties allow the placement of an optional text description or image beside the box. A radio button’s appearance is defined by the same properties used with the check box and button: Appearance and FlatStyle.
325
326
Chapter 7
■
Windows Forms Controls
Placing Radio Buttons in a Group Radio buttons are placed in groups that allow only one item in the group to be selected. For example, a 10-question multiple choice form would require 10 groups of radio buttons. Aside from the functional need, groups also provide an opportunity to create an aesthetically appealing layout. The frequently used GroupBox and Panel container controls support background images and styles that can enhance a form’s appearance. Figure 7-3 shows the striking effect (even more so in color) that can be achieved by placing radio buttons on top of a GroupBox that has a background image.
Figure 7-3
Radio buttons in a GroupBox that has a background image
Listing 7-1 presents a sample of the code that is used to place the radio buttons on the GroupBox control and make them transparent so as to reveal the background image.
Listing 7-1
Placing Radio Buttons in a GroupBox
using System.Drawing; using System.Windows.Forms; public class OperaForm : Form { private RadioButton radioButton1; private RadioButton radioButton2; private RadioButton radioButton3; private GroupBox groupBox1; public OperaForm() { this.groupBox1 = new GroupBox(); this.radioButton3 = new RadioButton(); this.radioButton2 = new RadioButton();
7.2 Button Classes, Group Box, Panel, and Label
Listing 7-1
Placing Radio Buttons in a GroupBox (continued)
this.radioButton1 = new RadioButton(); // All three radio buttons are created like this // For brevity only code for one button is included this.radioButton3.BackColor = Color.Transparent; this.radioButton3.Font = new Font("Microsoft Sans Serif", 8.25F, FontStyle.Bold); this.radioButton3.ForeColor = SystemColors.ActiveCaptionText; this.radioButton3.Location = new Point(16, 80); this.radioButton3.Name = "radioButton3"; this.radioButton3.Text = "Parsifal"; // Group Box this.groupBox1 = new GroupBox(); this.groupBox1.BackgroundImage = Image.FromFile("C:\\opera.jpg"); this.groupBox1.Size = new Size(120, 112); // Add radio buttons to groupbox groupBox1.Add( new Control[]{radioButton1,radiobutton2, radioButton3}); } }
Note that the BackColor property of the radio button is set to Color.Transparent. This allows the background image of groupBox1 to be displayed. By default, BackColor is an ambient property, which means that it takes the color of its parent control. If no color is assigned to the radio button, it takes the BackColor of groupBox1 and hides the image.
The GroupBox Class A GroupBox is a container control that places a border around its collection of controls. As demonstrated in the preceding example, it is often used to group radio buttons; but it is also a convenient way to organize and manage any related controls on a form. For example, setting the Enabled property of a group box to false disables all controls in the group box. constructor:
public GroupBox()
The constructor creates an untitled GroupBox having a default width of 200 pixels and a default height of 100 pixels.
327
328
Chapter 7
■
Windows Forms Controls
The Panel Class The Panel control is a container used to group a collection of controls. It’s closely related to the GroupBox control, but as a descendent of the ScrollableControl class, it adds a scrolling capability. constructor:
public Panel()
Its single constructor creates a borderless container area that has scrolling disabled. By default, a Panel takes the background color of its container, which makes it invisible on a form. Because the GroupBox and Panel serve the same purpose, the programmer is often faced with the choice of which to use. Here are the factors to consider in selecting one: • •
•
A GroupBox may have a visible caption, whereas the Panel does not. A GroupBox always displays a border; a Panel’s border is determined by its BorderStyle property. It may be set to BorderStyle.None, BorderStyle.Single, or BorderStyle.Fixed3D. A GroupBox does not support scrolling; a Panel enables automatic scrolling when its AutoScroll property is set to true.
A Panel offers no features to assist in positioning or aligning the controls it contains. For this reason, it is best used when the control layout is known at design time. But this is not always possible. Many applications populate a form with controls based on criteria known only at runtime. To support the dynamic creation of controls, .NET offers two layout containers that inherit from Panel and automatically position controls within the container: the FlowLayoutPanel and the TableLayoutPanel.
The FlowLayoutPanel Control Figure 7-4 shows the layout of controls using a FlowLayoutPanel.
Figure 7-4
FlowLayoutPanel
This “no-frills” control has a single parameterless constructor and two properties worth noting: a FlowDirection property that specifies the direction in which controls
7.2 Button Classes, Group Box, Panel, and Label
are to be added to the container, and a WrapControls property that indicates whether child controls are rendered on another row or truncated. The following code creates a FlowLayoutPanel and adds controls to its collection: FlowLayoutPanel flp = new FlowLayoutPanel(); flp.FlowDirection = FlowDirection.LefttoRight; // Controls are automatically positioned left to right flp.Controls.Add(Button1); flp.Controls.Add(Button2); flp.Controls.Add(TextBox1); flp.Controls.Add(Button3); this.Controls.Add(flp); // Add container to form
The FlowDirection enumerator members are BottomUp, LeftToRight, RighttoLeft, and TopDown. LefttoRight is the default.
TableLayoutPanel Control Figure 7-5 shows the grid layout that results from using a TableLayoutPanel container.
Figure 7-5
TableLayoutPanel organizes controls in a grid
This code segment creates a TableLayoutPanel and adds the same four controls used in the previous example. Container properties are set to define a layout grid that has two rows, two columns, and uses an Inset border style around each cell. Controls are always added to the container moving left-to-right, top-to-bottom. TableLayoutPanel tlp = new TableLayoutPanel(); // Causes the inset around each cell tlp.CellBorderStyle = TableLayoutPanelCellBorderStyle.Inset; tlp.ColumnCount = 2; // Grid has two columns tlp.RowCount = 2; // Grid has two rows // If grid is full add extra cells by adding column tlp.GrowStyle = TableLayoutPanelGrowStyle.AddColumns; // Padding (pixels)within each cell (left, top, right, bottom)
329
330
Chapter 7
■
Windows Forms Controls
tlp.Padding = new Padding(1,1,4,5); tlp.Controls.Add(Button1); tlp.Controls.Add(Button2); // Other controls added here
The GrowStyle property is worth noting. It specifies how controls are added to the container when all of its rows and columns are filled. In this example, AddColumns specifies that a column be added to accommodate new controls. The other options are AddRows and None; the latter causes an exception to be thrown if an attempt is made to add a control when the panel is filled.
The Label Class The Label class is used to add descriptive information to a form. constructor: public Label()
The constructor creates an instance of a label having no caption. Use the Text property to assign a value to the label. The Image, BorderStyle, and TextAlign properties can be used to define and embellish the label’s appearance.
Figure 7-6
Label containing an image and text
The following code creates the label shown in Figure 7-6: Label imgLabel = new Label(); imgLabel.BackColor= Color.White; Image img = Image.FromFile("c:\\rembrandt.jpg"); imgLabel.Image= img; imgLabel.ImageAlign= ContentAlignment.TopCenter; imgLabel.Text="Rembrandt"; imgLabel.TextAlign= ContentAlignment.BottomCenter; imgLabel.BorderStyle= BorderStyle.Fixed3D; imgLabel.Size = new Size(img.Width+10, img.Height+25);
7.3 PictureBox and TextBox Controls
One of its less familiar properties is UseMnemonic. By setting it to true and placing a mnemonic (& followed by a character) in the label’s text, you can create an access key. For example, if a label has a value of &Sum, pressing Alt-S shifts the focus to the control (based on tab order) following the label.
7.3
PictureBox and TextBox Controls
The PictureBox Class The PictureBox control is used to display images having a bitmap, icon, metafile, JPEG, GIF, or PNG format. It is a dynamic control that allows images to be selected at design time or runtime, and permits them to be resized and repositioned within the control. constructor: public PictureBox()
The constructor creates an empty (Image = null) picture box that has its SizeMode property set so that any images are displayed in the upper-left corner of the box. The two properties to be familiar with are Image and SizeMode. Image, of course, specifies the graphic to be displayed in the PictureBox. SizeMode specifies how the image is rendered within the PictureBox. It can be assigned one of four values from the PictureBoxSizeMode enumeration: 1. 2. 3. 4.
AutoSize. PictureBox is sized to equal the image. CenterImage. Image is centered in box and clipped if necessary. Normal. Image is place in upper-left corner and clipped if necessary. StretchImage. Image is stretched or reduced to fit in box.
Figure 7-7 illustrates some of the features of the PictureBox control. It consists of a form with three small picture boxes to hold thumbnail images and a larger picture box to display a full-sized image. The large image is displayed when the user double-clicks on a thumbnail image. The code, given in Listing 7-2, is straightforward. The event handler ShowPic responds to each DoubleClick event by setting the Image property of the large PictureBox ( bigPicture ) to the image contained in the thumbnail. Note that the original images are the size of bigPicture and are automatically reduced (by setting SizeMode) to fit within the thumbnail picture boxes.
331
332
Chapter 7
■
Windows Forms Controls
Figure 7-7 Thumbnail images in small picture boxes are displayed at full size in a larger viewing window
Listing 7-2
Working with Picture Boxes
using System; using System.Drawing; using System.Windows.Forms; public class ArtForm : Form { private PictureBox bigPicture; private PictureBox tn1; private PictureBox tn2; private PictureBox tn3; private Button btnClear; public ArtForm() { bigPicture = new PictureBox(); tn1 = new PictureBox(); tn2 = new PictureBox(); tn3 = new PictureBox(); btnClear = new Button(); bigPicture.Location = new Point(90, 30); bigPicture.Name = "bigPicture"; bigPicture.Size = new Size(160, 160); this.Controls.Add(bigPicture);
7.3 PictureBox and TextBox Controls
Listing 7-2
Working with Picture Boxes (continued)
// Define picturebox to hold first thumbnail image tn1.BorderStyle = BorderStyle.FixedSingle; tn1.Cursor = Cursors.Hand; tn1.Image = Image.FromFile("C:\\schiele1.jpg"); tn1.Location = new Point(8, 16); tn1.Name = "tn1"; tn1.Size = new Size(56, 56); tn1.SizeMode = PictureBoxSizeMode.StretchImage; this.Controls.Add(tn1); // Code for other thumbnails would go here // Button to clear picture box btnClear.Location = new Point(136, 192); btnClear.Name = "btnClear"; btnClear.Size = new Size(88, 24); btnClear.Text = "Clear Image"; this.Controls.Add(btnClear); btnClear.Click += new EventHandler(this.btnClear_Click); // Set up event handlers for double click events tn1.DoubleClick += new EventHandler(ShowPic); tn2.DoubleClick += new EventHandler(ShowPic); tn3.DoubleClick += new EventHandler(ShowPic); } static void Main() { Application.Run(new ArtForm()); } private void btnClear_Click(object sender, EventArgs e) { bigPicture.Image = null; // Clear image } private void ShowPic (object sender, EventArgs e) { // Sender is thumbnail image that is double clicked bigPicture.Image = ((PictureBox) sender).Image; } }
The TextBox Class The familiar TextBox is an easy-to-use control that has several properties that affect its appearance, but few that control its content. This leaves the developer with the task of setting up event handlers and data verification routines to control what is entered in the box.
333
334
Chapter 7
■
Windows Forms Controls
constructor: public TextBox()
The constructor creates a TextBox that accepts one line of text and uses the color and font assigned to its container. From such humble origins, the control is easily transformed into a multi-line text handling box that accepts a specific number of characters and formats them to the left, right, or center. Figure 7-8 illustrates some of the properties used to do this.
Figure 7-8
TextBox properties
The text is placed in the box using the Text property and AppendText method: txtPoetry.Text = "In Xanadu did Kubla Khan\r\na stately pleasure dome decree,"; txtPoetry.AppendText("\r\nWhere Alph the sacred river ran");
A couple of other TextBox properties to note are ReadOnly, which prevents text from being modified, and PasswordChar, which is set to a character used to mask characters entered—usually a password.
TextBoxes and Carriage Returns When storing data from a TextBox into a database, you want to make sure there are no special characters embedded in the text, such as a carriage return. If you look at the TextBox properties, you’ll find AcceptsReturn, which looks like a simple solution. Setting it to false should cause a TextBox to ignore the user pressing an Enter key. However, the name of this property is somewhat misleading. It only works when the form’s AcceptButton property is set to a button on the form. Recall that this property causes the associated button’s Click handler to be executed when the Enter key is pressed. If AcceptButton is not set (and the MultiLine property of the text box is set to true), the TextBox receives a newline (\r\n) when the Enter key is pushed.
7.4 ListBox, CheckedListBox, and ComboBox Classes
This leaves the developer with the task of handling unwanted carriage returns. Two approaches are available: capture the keystrokes as they are entered or extract the characters before storing the text. The first approach uses a keyboard event handler, which you should be familiar with from the previous chapter. // Set up event handler in constructor for TextBox txtPoetry txtPoetry.KeyPress += new KeyPressEventHandler(onKeyPress); private void onKeyPress( object sender, KeyPressEventArgs e) { if(e.KeyChar == (char)13) e.Handled = true; }
Setting Handled to true prevents the carriage return/linefeed from being added to the text box. This works fine for keyboard entry but has no effect on a cut-and-paste operation. To cover this occurrence, you can use the keyboard handling events described in Chapter 6 to prevent pasting, or you can perform a final verification step that replaces any returns with a blank or any character of your choice. txtPoetry.Text = txtPoetry.Text.Replace(Environment.NewLine," ");
Core Note Two common approaches for entering a carriage return/linefeed programmatically into a TextBox are txtPoetry.Text = "Line 1\r\nLine 2"; txtPoetry.Text = "Line 1"+Environment.NewLine+"Line 2";
7.4
ListBox, CheckedListBox, and ComboBox Classes
The ListBox Class The ListBox control is used to provide a list of items from which the user may select one or more items. This list is typically text but can also include images and objects. Other features of the ListBox include methods to perform text-based searches, sorting, multi-column display, horizontal and vertical scroll bars, and an easy way to override the default appearance and create owner-drawn ListBox items.
335
336
Chapter 7
■
Windows Forms Controls
constructor: public ListBox()
The constructor creates an empty ListBox. The code to populate a ListBox is typically placed in the containing form’s constructor or Form.Load event handler. If the ListBox.Sorted property is set to true, ListBox items are sorted alphabetically in ascending order. Also, vertical scroll bars are added automatically if the control is not long enough to display all items.
Adding Items to a ListBox A ListBox has an Items collection that contains all elements of the list. Elements can be added by binding the ListBox to a data source (described in Chapter 11, “ADO.NET”) or manually by using the Add method. If the Sorted property is false, the items are listed in the order they are entered. There is also an Insert method that places an item at a specified location. lstArtists.Items.Add("Monet"); lstArtists.Items.Add("Rembrandt"); lstArtists.Items.Add("Manet"); lstArtists.Items.Insert(0, "Botticelli"); //Place at top
Core Note To prevent a ListBox from repainting itself each time an item is added, execute the ListBox.BeginUpdate method prior to adding and ListBox.EndUpdate after the last item is added.
List boxes may also contain objects. Because an object may have many members, this raises the question of what is displayed in the TextBox list. Because by default a ListBox displays the results of an item’s ToString method, it is necessary to override this System.Object method to return the string you want displayed. The following class is used to create ListBox items: // Instances of this class will be placed in a ListBox public class Artist { public string BDate, DDate, Country; private string firstname; private string lastname; public Artist(string birth, string death, string fname, string lname, string ctry) { BDate = birth;
7.4 ListBox, CheckedListBox, and ComboBox Classes
DDate = death; Country = ctry; firstname = fname; lastname = lname; } public override string ToString() { return (lastname+" , "+firstname); } public string GetLName { get{ return lastname;} } public string GetFName { get{ return firstname;} } } ToString has been overridden to return the artist’s last and first names, which are displayed in the ListBox. The ListBox (Figure 7-9) is populated using these statements: lstArtists.Items.Add (new Artist("1832", lstArtists.Items.Add (new Artist("1840", lstArtists.Items.Add (new Artist("1606", lstArtists.Items.Add (new Artist("1445",
Figure 7-9
"1883", "Edouard", "Manet","Fr" )); "1926", "Claude", "Monet","Fr")); "1669", "Von Rijn", "Rembrandt","Ne")); "1510", "Sandre", "Botticelli","It"));
ListBox items: (A) Default and (B) Custom drawn
337
338
Chapter 7
■
Windows Forms Controls
Selecting and Searching for Items in a ListBox The SelectionMode property determines the number of items a ListBox allows to be selected at one time. It takes four values from the SelectionMode enumeration: None, Single, MultiSingle, and MultiExtended. MultiSingle allows selection by clicking an item or pressing the space bar; MultiExtended permits the use of the Shift and Ctrl keys. The SelectedIndexChanged event provides an easy way to detect when an item in a ListBox is selected. It is fired when the user clicks on an item or uses the arrow keys to traverse a list. A common use is to display further information about the selection in other controls on the form. Here is code that displays an artist’s dates of birth and death when the artist’s name is selected from the ListBox in Figure 7-9: // Set up event handler in constructor lstArtists.SelectedIndexChanged += new EventHandler(ShowArtist); // private void ShowArtist(object sender, EventArgs e) { // Cast to artist object in order to access properties Artist myArtist = lstArtists.SelectedItem as Artist; if (myArtist != null) { txtBirth.Text = myArtist.Dob; // Place dates in text boxes txtDeath.Text = myArtist.Dod; } }
The SelectedItem property returns the item selected in the ListBox. This object is assigned to myArtist using the as operator, which ensures the object is an Artist type. The SelectedIndex property can also be used to reference the selected item: myArtist = lstArtists.Items[lstArtists.SelectedIndex] as Artist;
Working with a multi-selection ListBox requires a different approach. You typically do not want to respond to a selection event until all items have been selected. One approach is to have the user click a button to signal that all choices have been made and the next action is required. All selections are exposed as part of the SelectedItems collection, so it is an easy matter to enumerate the items: foreach (Artist a in lstArtists.SelectedItems) MessageBox.Show(a.GetLName);
The SetSelected method provides a way to programatically select an item or items in a ListBox. It highlights the item(s) and fires the SelectedIndexChanged event. In this example, SetSelected is used to highlight all artists who were born in France:
7.4 ListBox, CheckedListBox, and ComboBox Classes
lstArtists.ClearSelected(); // Clear selected items for (int ndx =0; ndx < lstArtists.Items.Count-1; ndx ++) { Artist a = lstArtists.Items[ndx] as Artist; if (a.country == "Fr") lstArtists.SetSelected(ndx,true); }
Customizing the Appearance of a ListBox The ListBox, along with the ComboBox, MenuItem, and TabControl controls, is an owner-drawn control. This means that by setting a control property, you can have it fire an event when the control’s contents need to be drawn. A custom event handler takes care of the actual drawing. To enable owner drawing of the ListBox, the DrawMode property must be set to one of two DrawMode enumeration values: OwnerDrawFixed or OwnerDrawVariable. The former draws each item a fixed size; the latter permits variable-sized items. Both of these cause the DrawItem event to be fired and rely on its event handler to perform the drawing. Using the ListBox from the previous example, we can use the constructor to set DrawMode and register an event handler for the DrawItem event: lstArtists.DrawMode = DrawMode.OwnerDrawFixed; lstArtists.ItemHeight = 16; // Height (pixels) of item lstArtists.DrawItem += new DrawItemEventHandler(DrawList);
The DrawItemEventHandler delegate has two parameters: the familiar sender object and the DrawItemEventArgs object. The latter is of more interest. It contains properties related to the control’s appearance and state as well as a couple of useful drawing methods. Table 7-2 summarizes these. Table 7-2 DrawItemEventArgs Properties Member
Description
BackColor
Background color assigned to the control.
Bounds
Defines the coordinates of the item to be drawn as a Rectangle object.
Font
Returns the font assigned to the item being drawn.
ForeColor
Foreground color of the control. This is the color of the text displayed.
Graphics
Represents the surface (as a Graphics object) on which the drawing occurs.
339
340
Chapter 7
■
Windows Forms Controls
Table 7-2 DrawItemEventArgs Properties (continued) Member
Description
Index
The index in the control where the item is being drawn.
State
The state of the item being drawn. This value is a DrawItemState enumeration. For a ListBox, its value is Selected (1) or None(0).
DrawBackground()
Draws the default background.
DrawFocusRectangle()
Draws the focus rectangle around the item if it has focus.
Index is used to locate the item. Font, BackColor, and ForeColor return the current preferences for each. Bounds defines the rectangular area circumscribing the item and is used to indicate where drawing should occur. State is useful for making drawing decisions based on whether the item is selected. This is particularly useful when the ListBox supports multiple selections. We looked at the Graphics object briefly in the last chapter when demonstrating how to draw on a form. Here, it is used to draw in the Bounds area. Finally, the two methods, DrawBackground and DrawFocusRectangle, are used as their name implies. The event handler to draw items in the ListBox is shown in Listing 7-3. Its behavior is determined by the operation being performed: If an item has been selected, a black border is drawn in the background to highlight the selection; if an item is added, the background is filled with a color corresponding to the artist’s country, and the first and last names of the artist are displayed. The routine does require knowledge of some GDI+ concepts (see Chapter 8, “.NET Graphics Using GDI+”). However, the purpose of the methods should be clear from their name and context: FillRectangle fills a rectangular area defined by the Rectangle object, and DrawString draws text to the Graphics object using a font color defined by the Brush object. Figure 7-9(B) shows the output.
Listing 7-3
Event Handler to Draw Items in a ListBox
private void DrawList(object sender, DrawItemEventArgs e) { // Draw ListBox Items string ctry; Rectangle rect = e.Bounds; Artist a = lstArtists.Items[e.Index] as Artist; string artistName = a.ToString(); if ( (e.State & DrawItemState.Selected) == DrawItemState.Selected ) {
7.4 ListBox, CheckedListBox, and ComboBox Classes
Listing 7-3
Event Handler to Draw Items in a ListBox (continued)
// Draw Black border around the selected item e.Graphics.DrawRectangle(Pens.Black,rect); } else { ctry = a.Country; Brush b; // Object used to define backcolor // Each country will have a different backcolor b = Brushes.LightYellow; // Netherlands if (ctry == "Fr") b = Brushes.LightGreen; if (ctry == "It") b = Brushes.Yellow; e.Graphics.FillRectangle(b,rect);} e.Graphics.DrawString(artistName,e.Font, Brushes.Black,rect); } }
Other List Controls: the ComboBox and the CheckedListBox The ComboBox control is a hybrid control combining a ListBox with a TextBox (see Figure 7-10). Like the ListBox, it derives from the ListControl and thus possesses most of the same properties.
Figure 7-10 ComboBox and CheckedListBox controls are variations on ListBox
Visually, the ComboBox control consists of a text box whose contents are available through its Text property and a drop-down list from which a selected item is available through the SelectedItem property. When an item is selected, its textual representation is displayed in the text box window. A ComboBox can be useful in constructing questionnaires where the user selects an item from the drop-down list or, optionally, types in his own answer. Its construction is similar to the ListBox:
341
342
Chapter 7
■
Windows Forms Controls
ComboBox cbArtists = new ComboBox(); cbArtists.Size = new System.Drawing.Size(120, 21); cbArtists.MaxDropDownItems= 4; // Max number of items to display cbArtists.DropDownWidth = 140; // Width of drop-down portion cbArtists.Items.Add(new Artist("1832", "1883", "Edouard", "Manet","Fr" )); // Add other items here...
The CheckedListBox is a variation on the ListBox control that adds a check box to each item in the list. The default behavior of the control is to select an item on the first click, and check or uncheck it on the second click. To toggle the check on and off with a single click, set the CheckOnClick property to true. Although it does not support multiple selections, the CheckedListBox does allow multiple items to be checked and includes them in a CheckedItems collection. The code here loops through a collection of Artist objects that have been checked on the control: // List all items with checked box. foreach (Artist a in clBox.CheckedItems) MessageBox.Show(a.ToString()); // –> Monet, Claude
You can also iterate through the collection and explicitly determine the checked state: For (int i=0; I< clBox.Items.Count; i++) { if(clBox.GetItemCheckState(i) == CheckState.Checked) { Do something } else {do something if not checked } }
7.5
The ListView and TreeView Classes
The ListView Class ListView is another control that displays lists of information. It represents data rela-
tionally as items and subitems. The data can be represented in a variety of formats that include a multi-column grid and large or small icons to represent item data. Also, images and check boxes can adorn the control. Figure 7-11 illustrates the basic properties and methods used to lay out a Details view of the control—a format obviously tailored to displaying database tables. The first column contains text for an item—as well as a picture—the remaining columns contain subitems for the parent item.
7.5 The ListView and TreeView Classes
Figure 7-11
ListView control
Let’s look at how this style of the ListView is constructed.
Creating a ListView Object The ListView is created with a parameterless constructor: ListView listView1 = new ListView();
Define Appearance of ListView Object // Set the view to show details listView1.View = View.Details;
The View property specifies one of five layouts for the control: • • • • •
Details. An icon and item’s text are displayed in column one. Subitems are displayed in the remaining columns. LargeIcon. A large icon is shown for each item with a label below the icon. List. Each item is displayed as a small icon with a label to its right. The icons are arranged in columns across the control. SmallIcon. Each item appears in a single column as a small icon with a label to its right. *Tile. Each item appears as a full-size icon with the label and subitem details to the right of it. Only available for Windows XP and 2003.
343
344
Chapter 7
■
Windows Forms Controls
Core Note The ListView.View property can be changed at runtime to switch among the possible views. In fact, you may recognize that the view options correspond exactly to the View menu options available in Windows Explorer.
After the Details view is selected, other properties that define the control’s appearance and behavior are set: // Allow the user to rearrange columns listView1.AllowColumnReorder = true; // Select the entire row when selection is made listView1.FullRowSelect = true; // Display grid lines listView1.GridLines = true; // Sort the items in the list in ascending order listView1.Sorting = SortOrder.Ascending;
These properties automatically sort the items, permit the user to drag columns around to rearrange their order, and cause a whole row to be highlighted when the user selects an item.
Set Column Headers In a Details view, data is not displayed until at least one column is added to the control. Add columns using the Columns.Add method. Its simplest form is ListView.Columns.Add(caption, width, textAlign) Caption is the text to be displayed. Width specifies the column’s width in pixels. It is set to –1 to size automatically to the largest item in the column, or –2 to size to the width of the header. // Create column headers for the items and subitems listView1.Columns.Add("Artist", -2, HorizontalAlignment.Left); listView1.Columns.Add("Born", -2, HorizontalAlignment.Left); listView1.Columns.Add("Died", -2, HorizontalAlignment.Left); listView1.Columns.Add("Country", -2, HorizontalAlignment.Left);
The Add method creates and adds a ColumnHeader type to the ListView’s Columns collection. The method also has an overload that adds a ColumnHeader object directly:
7.5 The ListView and TreeView Classes
ColumnHeader cHeader: cHeader.Text = "Artist"; cHeader.Width = -2; cHeader.TextAlign = HorizontalAlignment.Left; ListView.Columns.Add(ColumnHeader cHeader);
Create ListView Items Several overloaded forms of the ListView constructor are available. They can be used to create a single item or a single item and its subitems. There are also options to specify the icon associated with the item and set the foreground and background colors. Constructors: public public public public public
ListViewItem(string text); ListViewItem(string[] items ); ListViewItem(string text,int imageIndex ); ListViewItem(string[] items,int imageIndex ); ListViewItem(string[] items,int imageIndex, Color foreColor,Color backColor,Font font);
The following code demonstrates how different overloads can be used to create the items and subitems shown earlier in Figure 7-8: // Create item and three subitems ListViewItem item1 = new ListViewItem("Manet",2); item1.SubItems.Add("1832"); item1.SubItems.Add("1883"); item1.SubItems.Add("France"); // Create item and subitems using a constructor only ListViewItem item2 = new ListViewItem (new string[] {"Monet","1840","1926","France"}, 3); // Create item and subitems with blue background color ListViewItem item3 = new ListViewItem (new string[] {"Cezanne","1839","1906","France"}, 1, Color.Empty, Color.LightBlue, null);
To display the items, add them to the Items collection of the ListView control: // Add the items to the ListView listView1.Items.AddRange( new ListViewItem[]{item1,item2,item3,item4,item5});
Specifying Icons Two collections of images can be associated with a ListView control as ImageList properties: LargeImageList, which contains images used in the LargeIcon view;
345
346
Chapter 7
■
Windows Forms Controls
and SmallImageList, which contains images used in all other views. Think of these as zero-based arrays of images that are associated with a ListViewItem by the imageIndex parameter in the ListViewItem constructor. Even though they are referred to as icons, the images may be of any standard graphics format. The following code creates two ImageList objects, adds images to them, and assigns them to the LargeImageList and SmallImageList properties: // Create two ImageList objects ImageList imageListSmall = new ImageList(); ImageList imageListLarge = new ImageList(); imageListLarge.ImageSize = new Size(50,50); // Set image size // Initialize the ImageList objects // Can use same images in both collections since they're resized imageListSmall.Images.Add(Bitmap.FromFile("C:\\botti.gif")); imageListSmall.Images.Add(Bitmap.FromFile("C:\\cezanne.gif")); imageListLarge.Images.Add(Bitmap.FromFile("C:\\botti.gif")); imageListLarge.Images.Add(Bitmap.FromFile("C:\\cezanne.gif")); // Add other images here // Assign the ImageList objects to the ListView. listView1.LargeImageList = imageListLarge; listView1.SmallImageList = imageListSmall; ListViewItem lvItem1 = new ListViewItem("Cezanne",1);
An index of 1 selects the cezanne.gif images as the large and small icons. Specifying an index not in the ImageList results in the icon at index 0 being displayed. If neither ImageList is defined, no icon is displayed. Figure 7-12 shows the ListView from Figure 7-11 with its view set to View.LargeIcon: listView1.View = View.LargeIcon;
Figure 7-12 LargeIcon view
7.5 The ListView and TreeView Classes
Working with the ListView Control Common tasks associated with the ListView control include iterating over the contents of the control, iterating over selected items only, detecting the item that has focus, and—when in Details view—sorting the items by any column. Following are some code segments to perform these tasks.
Iterating over All Items or Selected Items You can use foreach to create nested loops that select an item and then iterate through the collection of subitems for the item in the outside loop: foreach (ListViewItem lvi in listView1.Items) { string row = ""; foreach(ListViewItem.ListViewSubItem sub in lvi.SubItems) { row += " " + sub.Text; } MessageBox.Show(row); // List concatenated subitems }
There are a couple of things to be aware of when working with these collections. First, the first subitem (index 0) element actually contains the text for the item—not a subitem. Second, the ordering of subitems is not affected by rearranging columns in the ListView control. This changes the appearance but does not affect the underlying ordering of subitems. The same logic is used to list only selected items (MultiSelect = true permits multiple items to be selected). The only difference is that the iteration occurs over the ListView.SelectedItems collection: foreach (ListViewItem lvisel in listView1.SelectedItems)
Detecting the Currently Selected Item In addition to the basic control events such as Click and DoubleClick, the ListView control adds a SelectedIndexChanged event to indicate when focus is shifted from one item to another. The following code implements an event handler that uses the FocusedItem property to identify the current item: // Set this in the constructor listView1.SelectedIndexChanged += new EventHandler(lv_IndexChanged); // Handle SelectedIndexChanged Event private void lv_IndexChanged(object sender, System.EventArgs e)
347
348
Chapter 7
■
Windows Forms Controls
{ string ItemText = listView1.FocusedItem.Text; }
Note that this code can also be used with the Click events because they also use the EventHandler delegate. The MouseDown and MouseUp events can also be used to detect the current item. Here is a sample MouseDown event handler: private void listView1_MouseDown(object sender, MouseEventArgs e) { ListViewItem selection = listView1.GetItemAt(e.X, e.Y); if (selection != null) { MessageBox.Show("Item Selected: "+selection.Text); } }
The ListView.GetItemAt method returns an item at the coordinates where the mouse button is pressed. If the mouse is not over an item, null is returned.
Sorting Items on a ListView Control Sorting items in a ListView control by column values is a surprisingly simple feature to implement. The secret to its simplicity is the ListViewItemSorter property that specifies the object to sort the items anytime the ListView.Sort method is called. Implementation requires three steps: 1. Set up a delegate to connect a ColumnClick event with an event handler. 2. Create an event handler method that sets the ListViewItemSorter property to an instance of the class that performs the sorting comparison. 3. Create a class to compare column values. It must inherit the IComparer interface and implement the IComparer.Compare method. The following code implements the logic: When a column is clicked, the event handler creates an instance of the ListViewItemComparer class by passing it the column that was clicked. This object is assigned to the ListViewItemSorter property, which causes sorting to occur. Sorting with the IComparer interface is discussed in Chapter 4, “Working with Objects in C#”). // Connect the ColumnClick event to its event handler listView1.ColumnClick +=new ColumnClickEventHandler(ColumnClick); // ColumnClick event handler private void ColumnClick(object o, ColumnClickEventArgs e)
7.5 The ListView and TreeView Classes
{ // Setting this property immediately sorts the // ListView using the ListViewItemComparer object this.listView1.ListViewItemSorter = new ListViewItemComparer(e.Column); } // Class to implement the sorting of items by columns class ListViewItemComparer : IComparer { private int col; public ListViewItemComparer() { col = 0; // Use as default column } public ListViewItemComparer(int column) { col = column; } // Implement IComparer.Compare method public int Compare(object x, object y) { string xText = ((ListViewItem)x).SubItems[col].Text; string yText = ((ListViewItem)y).SubItems[col].Text; return String.Compare(xText, yText); } }
The TreeView Class As the name implies, the TreeView control provides a tree-like view of hierarchical data as its user interface. Underneath, its programming model is based on the familiar tree structure consisting of parent nodes and child nodes. Each node is implemented as a TreeNode object that can in turn have its own Nodes collection. Figure 7-13 shows a TreeView control that is used in conjunction with a ListView to display enum members of a selected assembly. (We’ll look at the application that creates it shortly.)
The TreeNode Class Each item in a tree is represented by an instance of the TreeNode class. Data is associated with each node using the TreeNode’s Text, Tag, or ImageIndex properties. The Text property holds the node’s label that is displayed in the TreeView control. Tag is an object type, which means that any type of data can be associated with the node by assigning a custom class object to it. ImageIndex is an index to an ImageList associated with the containing TreeView control. It specifies the image to be displayed next to the node.
349
350
Chapter 7
■
Figure 7-13
Windows Forms Controls
Using TreeView control (left) and ListView (right) to list enum values
In addition to these basic properties, the TreeNode class provides numerous other members that are used to add and remove nodes, modify a node’s appearance, and navigate the collection of nodes in a node tree (see Table 7-3). Table 7-3 Selected Members of the TreeNode Class Use
Member
Description
Appearance
BackColor, ForeColor
Sets the background color and text color of the node.
Expand(), Collapse()
Expands the node to display child nodes or collapses the tree so no child nodes are shown.
FirstNode, LastNode, NextNode, PrevNode
Returns the first or last node in the collection. Returns the next or previous node (sibling) relative to the current node.
Index
The index of the current node in the collection.
Parent
Returns the current node’s parent.
Nodes.Add(), Nodes.Remove(), Nodes.Insert(), Nodes.Clear()
Adds or removes a node to a Nodes collection. Insert adds a node at an indexed location, and Clear removes all tree nodes from the collection.
Clone()
Copies a tree node and entire subtree.
Navigation
Node Manipulation
7.5 The ListView and TreeView Classes
Let’s look at how TreeView and TreeNode members are used to perform fundamental TreeView operations.
Adding and Removing Nodes The following code creates the tree in Figure 7-14 using a combination of Add, Insert, and Clone methods. The methods are performed on a preexisting treeView1 control. TreeNode tNode; // Add parent node to treeView1 control tNode = treeView1.Nodes.Add("A"); // Add child node: two overloads available tNode.Nodes.Add(new TreeNode("C")); tNode.Nodes.Add("D")); // Insert node after C tNode.Nodes.Insert(1,new TreeNode("E")); // Add parent node to treeView1 control tNode = treeView1.Nodes.Add("B");
B
A
C
E
D
A
C
E
D
Figure 7-14 TreeView node representation
At this point, we still need to add a copy of node A and its subtree to the parent node B. This is done by cloning the A subtree and adding it to node B. Node A is referenced as treeView1.Nodes[0] because it is the first node in the control’s collection. Note that the Add method appends nodes to a collection, and they can be referenced by their zero-based position within the collection: // Clone first parent node and add to node B TreeNode clNode = (TreeNode) treeView1.Nodes[0].Clone(); tNode.Nodes.Add(clNode); // Add and remove node for demonstration purposes tNode.Nodes.Add("G"); tNode.Nodes.Remove(tNode.LastNode);
351
352
Chapter 7
■
Windows Forms Controls
Iterating Through the Nodes in a TreeView As with any collection, the foreach statement provides the easiest way to loop through the collection’s members. The following statements display all the top-level nodes in a control: foreach (TreeNode tn in treeView1.Nodes) { MessageBox.Show(tn.Text); // If (tn.IsVisible) true if node is visible // If (tn.IsSelected) true if node is currently selected }
An alternate approach is to move through the collection using the TreeNode.NextNode property: tNode = treeView1.Nodes[0]; while (tNode != null) { MessageBox.Show(tNode.Text); tNode = tNode.NextNode; }
Detecting a Selected Node When a node is selected, the TreeView control fires an AfterSelect event that passes a TreeViewEventArgs parameter to the event handling code. This parameter identifies the action causing the selection and the node selected. The TreeView example that follows illustrates how to handle this event. You can also handle the MouseDown event and detect the node using the GetNodeAt method that returns the node—if any—at the current mouse coordinates. private void treeView1_MouseDown(object sender, MouseEventArgs e) { TreeNode tn = treeView1.GetNodeAt(e.X, e.Y); // You might want to remove the node: tn.Remove() }
A TreeView Example That Uses Reflection This example demonstrates how to create a simple object browser (refer to Figure 7-13) that uses a TreeView to display enumeration types for a specified assembly. When a node on the tree is clicked, the members for the selected enumeration are displayed in a ListView control. Information about an assembly is stored in its metadata, and .NET provides classes in the System.Reflection namespace for exposing this metadata. The code in Listing 7-4 iterates across the types in an assembly to build the TreeView. The
7.5 The ListView and TreeView Classes
parent nodes consist of unique namespace names, and the child nodes are the types contained in the namespaces. To include only enum types, a check is made to ensure that the type inherits from System.Enum.
Listing 7-4
Using a TreeView and Reflection to List Enums in an Assembly
using System.Reflection; // private void GetEnums() { TreeNode tNode=null; Assembly refAssembly ; Hashtable ht= new Hashtable(); // Keep track of namespaces string assem = AssemName.Text; // Textbox with assembly name tvEnum.Nodes.Clear(); // Remove all nodes from tree // Load assembly to be probed refAssembly = Assembly.Load(assem); foreach (Type t in refAssembly.GetTypes()) { // Get only types that inherit from System.Enum if(t.BaseType!=null && t.BaseType.FullName=="System.Enum") { string myEnum = t.FullName; string nSpace = myEnum.Substring(0,myEnum.LastIndexOf(".")); myEnum= myEnum.Substring(myEnum.LastIndexOf(".")+1) ; // Determine if namespace in hashtable if( ht.Contains(nSpace)) { // Find parent node representing this namespace foreach (TreeNode tp in tvEnum.Nodes) { if(tp.Text == myEnum) { tNode=tp; break;} } } else { // Add parent node to display namespace tNode = tvEnum.Nodes.Add(nSpace); ht.Add(nSpace,nSpace); }
353
354
Chapter 7
■
Listing 7-4
Windows Forms Controls
Using a TreeView and Reflection to List Enums in an Assembly (continued)
// Add Child - name of enumeration TreeNode cNode = new TreeNode(); cNode.Text= myEnum; cNode.Tag = t; // Contains specific enumeration tNode.Nodes.Add(cNode); } } }
Notice how reflection is used. The static Assembly.Load method is used to create an Assembly type. The Assembly.GetTypes is then used to return a Type array containing all types in the designated assembly. refAssembly = Assembly.Load(assem); foreach (Type t in refAssembly.GetTypes())
The Type.FullName property returns the name of the type, which includes the namespace. This is used to extract the enum name and the namespace name. The Type is stored in the Tag field of the child nodes and is used later to retrieve the members of the enum. After the TreeView is built, the final task is to display the field members of an enumeration when its node is clicked. This requires registering an event handler to be notified when an AfterSelect event occurs: tvEnum.AfterSelect += new TreeViewEventHandler(tvEnum_AfterSelect);
The event handler identifies the selected node from the TreeViewEventArgs.Node property. It casts the node’s Tag field to a Type class (an enumerator in this case) and uses the GetMembers method to retrieve the type’s members as MemberInfo types. The name of each field member—exposed by the MemberInfo.Name property—is displayed in the ListView: // ListView lView; // lView.View = View.List; private void tvEnum_AfterSelect(Object sender, TreeViewEventArgs e) { TreeNode tn = e.Node; // Node selected ListViewItem lvItem;
7.6 The ProgressBar, Timer, and StatusStrip Classes
if(tn.Parent !=null) // Exclude parent nodes { lView.Items.Clear(); // Clear ListView before adding items Type cNode = (Type) tn.Tag; // Use Reflection to iterate members in a Type foreach (MemberInfo mi in cNode.GetMembers()) { if(mi.MemberType==MemberTypes.Field && mi.Name != "value__" ) // skip this { lView.Items.Add(mi.Name); } } } }
7.6
The ProgressBar, Timer, and StatusStrip Classes
The ProgressBar and Timer are lightweight controls that have complementary roles in an application: The Timer initiates action and the ProgressBar reflects the status of an operation or action. In fact, the Timer is not a control, but a component that inherits from the ComponentModel.Component class. It is used most often in processes to regulate some background activity. This may be a periodic update to a log file or a scheduled backup of data. A ProgressBar, on the other hand, provides visual feedback regarding the progress of an operation—such as file copying or steps in an installation. The third class discussed in this section is the StatusStrip, which is often used in conjunction with a timer and ProgressBar. It’s rendered on a form as a strip divided into one or more sections or panes that provide status information. Each section is implemented as a control that is added to the StatusStrip container. For a control to be included in the StatusStrip, it must inherit from the ToolStripItem class.
Building a StatusStrip Let’s now build a form that includes a multi-pane StatusStrip. As shown in Figure 7-15, the strip consists of a label, progress bar, and panel controls. The label (ToolStripLabel) provides textual information describing the overall status of the application. The progress bar is implemented as a ToolStripProgressBar object. It is functionally equivalent to a ProgressBar, but inherits from ToolStripItem. A
355
356
Chapter 7
■
Windows Forms Controls
StatusStripPanel shows the elapsed time since the form was launched. An event handler that is triggered by a timer updates both the progress bar and clock panel every five seconds.
Figure 7-15
StatusStrip with Label, ProgressBar, and Panel
Listing 7-5 contains the code to create the StatusStrip. The left and right ends of the progress bar are set to represent the values 0 and 120, respectively. The bar is set to increase in a step size of 10 units each time the PerformStep method is executed. It recycles every minute. The Timer controls when the bar is incremented and when the elapsed time is updated. Its Interval property is set to a value that controls how frequently its Tick event is fired. In this example, the event is fired every 5 seconds, which results in the progress bar being incremented by 10 units and the elapsed time by 5 seconds.
Listing 7-5
StatusStrip That Uses a ProgressBar and Timer
// These variables have class scope Timer currTimer; StatusStrip statusStrip1; StatusStripPanel panel1; ToolStripProgressBar pb; DateTime startDate = DateTime.Now; private void BuildStrip() { currTimer = new Timer(); currTimer.Enabled = true; currTimer.Interval = 5000; // Fire tick event every 5 seconds currTimer.Tick += new EventHandler(timer_Tick); // Panel to contain elapsed time
7.6 The ProgressBar, Timer, and StatusStrip Classes
Listing 7-5
StatusStrip That Uses a ProgressBar and Timer (continued)
panel1 = new StatusStripPanel(); panel1.BorderStyle = Border3DStyle.Sunken; panel1.Text = "00:00:00"; panel1.Padding = new Padding(2); panel1.Name = "clock"; panel1.Alignment = ToolStripItemAlignment.Tail; //Right align // Label to display application status ToolStripLabel ts = new ToolStripLabel(); ts.Text = "Running..."; // ProgressBar to show time elapsing pb = new ToolStripProgressBar(); pb.Step = 10; // Size of each step or increment pb.Minimum = 0; pb.Maximum = 120; // Allow 12 steps // Status strip to contain components statusStrip1 = new StatusStrip(); statusStrip1.Height = 20; statusStrip1.AutoSize = true; // Add components to strip statusStrip1.Items.AddRange(new ToolStripItem[] { ts, pb, panel1 } ); this.Controls.Add(statusStrip1); } private void timer_Tick(object sender, EventArgs e) { // Get difference between current datetime // and form startup time TimeSpan ts = DateTime.Now.Subtract(startDate); string elapsed = ts.Hours.ToString("00") + ":" + ts.Minutes.ToString("00") + ":" + ts.Seconds.ToString("00"); ((StatusStripPanel)statusStrip1.Items[ "clock"]).Text= elapsed; // Advance progress bar if (pb.Value == pb.Maximum) pb.Value = 0; pb.PerformStep(); // Increment progress bar }
The StatusStripPanel that displays the elapsed time has several properties that control its appearance and location. In addition to those shown here, it has an Image property that allows it to display an image. The StatusStripPanel class
357
358
Chapter 7
■
Windows Forms Controls
inherits from the ToolStripLabel class that is used in the first pane. Both can be used to display text, but the panel includes a BorderStyle property that ToolStripLabel lacks.
7.7
Building Custom Controls
At some point, you will face a programming task for which a standard WinForms control does not provide the functionality you need. For example, you may want to extend a TextBox control so that its background color changes according to its content, group a frequently used set of radio buttons into a single control, or create a new control that shows a digital clock face with the date underneath. These needs correspond to the three principal types of custom controls: 1. A control that derives from an existing control and extends its functionality. 2. A control that can serve as container to allow multiple controls to interact. This type of control is referred to as a user control. It derives directly from System.Windows.Forms.UserControl rather than Control, as do standard controls. 3. A control that derives directly from the Control class. This type of control is built “from scratch,” and it is the developer’s responsibility to draw its GUI interface and implement the methods and properties that allow it to be manipulated by code. Let’s now look at how to extend an existing control and create a user control.
Extending a Control The easiest way to create a custom control is to extend an existing one. To demonstrate this, let’s derive a TextBox that accepts only digits. The code is quite simple. Create a new class NumericTextBox with TextBox as its base class. The only code required is an event handler to process the KeyPress event and accept only a digit. class NumericTextBox: TextBox { public NumericTextBox() { this.KeyPress += new KeyPressEventHandler(TextBoxKeyPress); } protected void TextBoxKeyPress(object sender, KeyPressEventArgs e)
7.7 Building Custom Controls
{ if (! char.IsDigit(e.KeyChar)) e.Handled = true; } }
After the extended control is compiled into a DLL file, it can be added to any form.
Building a Custom UserControl Think of a user control as a subform. Like a form, it provides a container surface on which related widgets are placed. When compiled, the entire set of controls is treated as a single user control. Of course, users still can interact directly with any of the member controls. Programmatic and design-time access to control members is available through methods and properties defined on the user control. The easiest way to design a control is with an IDE such as Visual Studio.NET (VS.NET), which makes it easy to position and size controls. The usual way to create a user control in VS.NET is to open a project as a Windows Control Library type. This immediately brings up a control designer window. The design window can also be accessed in a Windows Application by selecting Project – Add User Control from the top menu bar or right-clicking on the Solution Explorer and selecting Add – Add User Control. Although VS.NET can speed up the process of creating a control, it does not generate any proprietary code that cannot be duplicated using a text editor.
A UserControl Example As an example, let’s create a control that can be used to create a questionnaire. The control consists of a label whose value represents the question, and three radio buttons contained on a panel control that represent the user’s choice of answers. The control exposes three properties: one that assigns the question to the label, one to set the background color of the panel control, and another that identifies the radio button associated with the user’s answer. Figure 7-16 shows the layout of the user control and the names assigned to each contained control. Here is how the members are represented as fields within the UserControl1 class: public class UserControl1 : System.Windows.Forms.UserControl { private Panel panel1; private RadioButton radAgree; private RadioButton radDisagree; private RadioButton radUn; private Label qLabel;
359
360
Chapter 7
■
Windows Forms Controls
Figure 7-16
Layout of a custom user control
Listing 7-6 contains the code for three properties: SetQ that sets the label’s text property to the question, PanelColor that sets the color of the panel, and Choice, which returns the answer selected by the user as a Choices enum type.
Listing 7-6
Implementing Properties for a Custom User Control
public enum Choices { Agree = 1, DisAgree = 2, Undecided = 3, } public string SetQ { set {qLabel.Text = value;} get {return(qLabel.Text);} } public Color PanelColor { set {panel1.BackColor= value;} get {return(panel1.BackColor);} } public Choices Choice { get {
7.7 Building Custom Controls
Listing 7-6
Implementing Properties for a Custom User Control (continued)
Choices usel; usel = Choices.Undecided; if (radDisagree.Checked) usel= Choices.DisAgree; if (radAgree.Checked) usel = Choices.Agree; return(usel);} } }
Using the Custom User Control If the user control is developed as part of a VS.NET Windows Application project, it is automatically added to the tool box under the Windows Forms tab. Simply select it and drop it onto the form. Otherwise, you have to right-click on a tool box tab, select Customize ToolBox, browse for the control, and add it to the tool box.
Figure 7-17
Custom user controls on a form
Figure 7-17 provides an example of using this new control. In this example, we place two control instances on the form and name them Q1 and Q2: private usercontrol.UserControl1 Q1; private usercontrol.UserControl1 Q2;
361
362
Chapter 7
■
Windows Forms Controls
The properties can be set in the constructor or at runtime in the Form.Load event handler. If using VS.NET, the properties can be set at design time using the Property Browser. Q1.SetQ = "The economy is performing well"; Q2.SetQ = "I'm not worried about the budget deficit."; Q1.PanelColor = Color.Beige;
The final step in the application is to do something with the results after the questionnaire has been completed. The following code iterates through the controls on the form when the button is clicked. When a UserControl1 type is encountered, its Choice property is used to return the user’s selection. private void button1_Click(object sender, System.EventArgs e) { foreach (Control ct in this.Controls) { if (ct is usercontrol.UserControl1) { UserControl1 uc = (UserControl1)ct; // Display control name and user's answer MessageBox.Show(ct.Name+" "+ uc.Choice.ToString()); } } }
Working with the User Control at Design Time If you are developing an application with VS.NET that uses this custom control, you will find that the Property Browser lists all of the read/write properties. By default, they are placed in a Misc category and have no description associated with them. To add a professional touch to your control, you should create a category for the control’s events and properties and add a textual description for each category member. The categories and descriptions available in the Property Browser come from metadata based on attributes attached to a type’s members. Here is an example of attributes added to the PanelColor property: [Browsable(true), Category("QControl"), Description("Color of panel behind question block")] public Color PanelColor { set {panel1.BackColor = value;} get {return (panel1.BackColor);} }
7.8 Using Drag and Drop with Controls
The Browsable attribute indicates whether the property is to be displayed in the browser. The default is true. The other two attributes specify the category under which the property is displayed and the text that appears below the Property Browser when the property is selected. Always keep in mind that the motive for creating custom user controls is reusability. There is no point in spending time creating elaborate controls that are used only once. As this example illustrates, they are most effective when they solve a problem that occurs repeatedly.
7.8
Using Drag and Drop with Controls
The ability to drag data from one control and drop it onto another has long been a familiar feature of GUI programming. .NET supports this feature with several classes and enumerations that enable a control to be the target and/or source of the drag-and-drop operation.
Overview of Drag and Drop The operation requires a source control that contains the data to be moved or copied, and a target control that receives the dragged data. The source initiates the action in response to an event—usually a MouseDown event. The source control’s event handler begins the actual operation by invoking its DoDragDrop method. This method has two parameters: the data being dragged and a DragDropEffects enum type parameter that specifies the effects or actions the source control supports (see Table 7-4). Table 7-4 DragDropEffects Enumeration Member
Description
All
The data is moved to the target control, and scrolling occurs in the target control to display the newly positioned data.
Copy
Data is copied from target to source.
Link
Data from the source is linked to the target.
Move
The data is moved from the source to the target control.
None
The target control refuses to accept data.
Scroll
Scrolling occurs or will occur on the target control.
363
364
Chapter 7
■
Windows Forms Controls
As the mouse moves across the form, the DoDragDrop method determines the control under the current cursor location. If this control has its AllowDrop property set to true, it is a valid drop target and its DragEnter event is raised. The DragEnter event handler has two tasks: to verify that the data being dragged is an acceptable type and to ensure the requested action (Effect) is acceptable. When the actual drop occurs, the destination control raises a DragDrop event. This event handler is responsible for placing the data in the target control (see Figure 7-18).
Source Control MouseDown event
Target Control
DragEnter event
DoDragDrop( data, DragDropEffects.Move)
Perform any housekeeping.
N Is data type correct? Y DragDrop event
Figure 7-18 Sequence of events in drag-and-drop operation
After the DragDrop event handler finishes, the source control performs any cleanup operations. For example, if the operation involves moving data—as opposed to copying—the data must be removed from the source control. To demonstrate these ideas, let’s create an application that assigns players to a team from a roster of available players (see Figure 7-19). Team A is created by dragging names from the Available Players to the Team A list. Both lists are implemented with list boxes, and the Available Players list is set for single selection. A name is selected by pressing the right mouse button and dragging the name to the target list. To add some interest, holding the Ctrl key copies a name rather than moving it. After the form and controls are created, the first step is to set up the source control (lstPlayers) to respond to the MouseDown event and the target control (lstTeamA) to handle the DragEnter and DragDrop events: lstPlayers.MouseDown += new MouseEventHandler(Players_MouseDown); lstTeamA.DragEnter += new DragEventHandler(TeamA_DragEnter); lstTeamA.DragDrop += new DragEventHandler(TeamA_Drop);
7.8 Using Drag and Drop with Controls
The next step is to code the event handlers on the source and target control(s) that implement the drag-and-drop operation.
Figure 7-19
Drag-and-drop example
Source Control Responsibilities The MouseDown event handler for the source ListBox first checks to ensure that an item has been selected. It then calls DoDragDrop, passing it the value of the selected item as well as the acceptable effects: Move and Copy. The DragDropEffects enumeration has a FlagsAttribute attribute, which means that any bitwise combination of its values can be passed. The value returned from this method is the effect that is actually used by the target. The event handler uses this information to perform any operations required to implement the effect. In this example, a move operation means that the dragged value must be removed from the source control.
Listing 7-7
Initiating a Drag-and-Drop Operation from the Source Control
private void Players_MouseDown(object sender, MouseEventArgs e) { if ( lstPlayers.SelectedIndex >=0) { string players; int ndx = lstPlayers.SelectedIndex; DragDropEffects effect; players = lstPlayers.Items[ndx].ToString(); if(players != "") {
365
366
Chapter 7
■
Windows Forms Controls
Listing 7-7
Initiating a Drag-and-Drop Operation from the Source Control (continued)
// Permit target to move or copy data effect = lstPlayers.DoDragDrop(players, DragDropEffects.Move | DragDropEffects.Copy); // Remove item from ListBox since move occurred if (effect == DragDropEffects.Move) lstPlayers.Items.RemoveAt(ndx); } } }
Target Control Responsibilities The destination control must implement the event handlers for the DragEnter and DragDrop events. Both of these events receive a DragEventArgs type parameter (see Table 7-5) that contains the information required to process the drag-and-drop event. Table 7-5 DragEventArgs Properties Member
Description
AllowedEffect
The effects that are supported by the source control. Example to determine if Move is supported: if ((e.AllowedEffect & DragDropEffects.Move) == DragDropEffects.Move)
Data
Returns the IDataObject that contains data associated with this operation. This object implements methods that return information about the data. These include GetData, which fetches the data, and GetDataPresent, which checks the data type.
Effect
Gets or sets the target drop effect.
KeyState
Returns the state of the Alt key, Ctrl key, Shift key, and mouse buttons as an integer: 1—Left mouse button 8—Ctrl key 2—Right mouse button 16—Middle mouse button 4—Shift key 32—Alt key
X, Y
x and y coordinates of the mouse pointer.
7.8 Using Drag and Drop with Controls
The Data, Effect, and KeyState members are used as follows: • •
•
•
Data.GetDataPresent is used by the DragEnter event handler to
ensure that the data is a type the target control can process. The DragDrop event handler uses Data.GetData to access the data being dragged to it. The parameter to this method is usually a static field of the DataFormats class that specifies the format of the returned data. The DragEnter event handler uses KeyState to determine the status of the mouse and keys in order to determine the effect it will use to process the data. Recall that in this example, pressing the Ctrl key signals that data is to copied rather than moved. Effect is set by the DragEnter event handler to notify the source as to how—or if—it processed the data. A setting of DragDropEffects.None prevents the DragDrop event from firing.
Listing 7-8 shows the code for the two event handlers.
Listing 7-8
Handling the DragEnter and DragDrop Events
[FlagsAttribute] enum KeyPushed { // Corresponds to DragEventArgs.KeyState values LeftMouse = 1, RightMouse = 2, ShiftKey = 4, CtrlKey = 8, MiddleMouse = 16, AltKey = 32, } private void TeamA_DragEnter(object sender, DragEventArgs e) { KeyPushed kp = (KeyPushed) e.KeyState; // Make sure data type is string if (e.Data.GetDataPresent(typeof(string))) { // Only accept drag with left mouse key if ( (kp & KeyPushed.LeftMouse) == KeyPushed.LeftMouse) { if ((kp & KeyPushed.CtrlKey) == KeyPushed.CtrlKey) { e.Effect = DragDropEffects.Copy; // Copy
367
368
Chapter 7
■
Windows Forms Controls
Listing 7-8
Handling the DragEnter and DragDrop Events (continued)
} else { e.Effect = DragDropEffects.Move; }
// Move
} else // Is not left mouse key { e.Effect = DragDropEffects.None; } } else // Is not a string { e.Effect = DragDropEffects.None; } } // Handle DragDrop event private void TeamA_Drop(object sender, DragEventArgs e) { // Add dropped data to TextBox lstTeamA.Items.Add( (string) e.Data.GetData(DataFormats.Text)); }
An enum is created with the FlagsAttributes attribute to make checking the KeyState value easier and more readable. The logical “anding” of KeyState with the value of the CtrlKey (8) returns a value equal to the value of the CtrlKey if the Ctrl key is pressed. A control can serve as source and target in the same application. You could make this example more flexible by having the list boxes assume both roles. This would allow you to return a player from lstTeamA back to the lstPlayers ListBox. All that is required is to add the appropriate event handlers.
Core Note Drag and drop is not just for text. The DataFormats class predefines the formats that can be accepted as static fields. These include Bitmap, PenData, WaveAudio, and numerous others.
7.9 Using Resources
7.9
Using Resources
Figure 7-7, shown earlier in the chapter, illustrates the use of PictureBox controls to enlarge and display a selected thumbnail image. Each thumbnail image is loaded into the application from a local file: tn1 = new PictureBox(); tn1.Image = Image.FromFile("c:\\schiele1.jpg");
This code works fine as long as the file schiele1.jpg exists in the root directory of the user’s computer. However, relying on the directory path to locate this file has two obvious disadvantages: The file could be deleted or renamed by the user, and it’s an external resource that has to be handled separately from the code during installation. Both problems can be solved by embedding the image in the assembly rather than treating it as an external resource. Consider a GUI application that is to be used in multiple countries with different languages. The challenge is to adapt the screens to each country. At a minimum, this requires including text in the native language, and may also require changing images and the location of controls on the form. The ideal solution separates the logic of the program from the user interface. Such a solution treats the GUI for each country as an interchangeable resource that is loaded based on the culture settings (the country and language) of the computer. The common denominator in these two examples is the need to bind an external resource to an application. .NET provides special resource files that can be used to hold just about any nonexecutable data such as strings, images, and persisted data. These resource files can be included in an assembly—obviating the need for external files—or compiled into satellite assemblies that can be accessed on demand by an application’s main assembly. Let’s now look at the basics of working with resource files and how to embed them in assemblies; then, we will look at the role of satellite assemblies in localized applications.
Working with Resource Files Resource files come in three formats: *.txt files in name/value format, *.resx files in an XML format, and *.resources files in a binary format. Why three? The text format provides an easy way to add string resources, the XML version supports both strings and other objects such as images, and the binary version is the binary equivalent of the XML file. It is the only format that can be embedded in an assembly—the other formats must be converted into a .resources file before they can be linked to an assembly. Figure 7-20 illustrates the approaches that can be used to create a .resources file.
369
370
Chapter 7
■
Windows Forms Controls
Resources
.txt
• strings • images • cursors ResXResourceWriter
resgen.exe
ResourceWriter
.resx
.resources
resgen.exe
Figure 7-20 A .resources file can be created from a text file, resources, or a .resx file
The System.Resources namespace contains the types required to manipulate resource files. It includes classes to read from and write to both resource file formats, as well as load resources from an assembly into a program.
Creating Resource Strings from a Text File Resource files containing string values are useful when it is necessary for a single application to present an interface that must be customized for the environment in which it runs. A resource file eliminates the need to code multiple versions of an application; instead, a developer creates a single application and multiple resource files that contain the interface captions, text, messages, and titles. For example, an English version of an application would have the English resource file embedded in its assembly; a German version would embed the German resource file. Creating resource strings and accessing them in an application requires four steps: 1. Create a text file with the name/value strings to be used in the application. The file takes this format: ;German version (this is a comment) Language=German Select=Wählen Sie aus Page=Seite Previous=Vorherig Next=Nächst
2. Convert the text file to a .resources file using the Resource File Generator utility resgen.exe: > resgen german.txt
german.resources
Note that the text editor used to create the text file should save it using UTF-8 encoding, which resgen expects by default.
7.9 Using Resources
3. Use the System.Resources.ResourceManager class to read the strings from the resource file. As shown here, the ResourceManager class accepts two arguments: the name of the resource file and the assembly containing it. The Assembly class is part of the System. Reflection namespace and is used in this case to return the current assembly. After the resource manager is created, its GetString method is used by the application to retrieve strings from the resource file by their string name: // new ResourceManager(resource file, assembly) ResourceManager rm = new ResourceManager( "german",Assembly.GetExecutingAssembly()); nxtButton.Text= rm.GetString("Next");
4. For this preceding code to work, of course, the resource file must be part of the application’s assembly. It’s bound to the assembly during compilation: csc /t:exe /resource:german.resources myApp.cs
Using the ResourceWriter Class to Create a .resources File The preceding solution works well for adding strings to a resource file. However, a resource file can also contain other objects such as images and cursor shapes. To place these in a .resources file, .NET offers the System.Resources.ResourceWriter class. The following code, which would be placed in a utility or helper file, shows how to create a ResourceWriter object and use its AddResource method to store a string and image in a resource file: IResourceWriter writer = new ResourceWriter( "myResources.resources"); // .Resources output file Image img = Image.FromFile(@"c:\schiele1.jpg"); rw.AddResource("Page","Seite"); // Add string rw.AddResource("artistwife",img); // Add image rw.Close(); // Flush resources to the file
Using the ResourceManager Class to Access Resources As we did with string resources, we use the ResourceManager class to access object resources from within the application. To illustrate, let’s return to the code presented at the beginning of this section: tn1.Image = Image.FromFile("C:\\schiele1.jpg");
371
372
Chapter 7
■
Windows Forms Controls
The ResourceManager allows us to replace the reference to an external file, with a reference to this same image that is now part of the assembly. The GetString method from the earlier example is replaced by the GetObject method: ResourceManager rm = new ResourceManager("myresources", Assembly.GetExecutingAssembly()); // Extract image from resources in assembly tn1.Image = (Bitmap) rm.GetObject("artistwife");
Using the ResXResourceWriter Class to Create a .resx File The ResXResourceWriter class is similar to the ResourceWriter class except that it is used to add resources to a .resx file, which represents resources in an intermediate XML format. This format is useful when creating utility programs to read, manage, and edit resources—a difficult task to perform with the binary .resources file. ResXResourceWriter rwx = new ResXResourceWriter(@"c:\myresources.resx"); Image img = Image.FromFile(@"c:\schiele1.jpg"); rwx.AddResource("artistwife",img); // Add image rwx.Generate(); // Flush all added resources to the file
The resultant file contains XML header information followed by name/value tags for each resource entry. The actual data—an image in this case—is stored between the value tags. Here is a section of the file myresources.resx when viewed in a text editor:
---Actual Image bytes go here ---
Note that although this example stores only one image in the file, a .resx file can contain multiple resource types.
Using the ResXResourceReader Class to Read a .resx file The ResXResourceReader class provides an IDictionaryEnumerator (see Chapter 4) that is used to iterate through the tag(s) in a .resx file. This code segment lists the contents of a resource file:
7.9 Using Resources
ResXResourceReader rrx = new ResXResourceReader("c:\\myresources.resx"); // Enumerate the collection of tags foreach (DictionaryEntry de in rrx) { MessageBox.Show("Name: "+de.Key.ToString()+"\nValue: " + de.Value.ToString()); // Output --> Name: artistwife // --> Value: System.Drawing.Bitmap } rrx.Close();
Converting a .resx File to a .resources File The .resx file is converted to a .resources file using resgen.exe: resgen myresources.resx
myresources.resources
If the second parameter is not included, the output file will have the same base name as the source file. Also, note that this utility can be used to create a .resources file from a .resx file. The syntax is the same as in the preceding example—just reverse the parameters.
VS.NET and Resources Visual Studio.NET automatically creates a .resx file for each form in a project and updates them as more resources are added to the project. You can see the resource file(s) by selecting the Show All Files icon in the Solution Explorer. When a build occurs, .resources files are created from the .resx files. In the code itself, a ResourceManager object is created to provide runtime access to the resources: ResourceManager resources = new ResourceManager(typeof(Form1));
Using Resource Files to Create Localized Forms In .NET vernacular, a localized application is one that provides multi-language support. This typically means providing user interfaces that display text and images customized for individual countries or cultures. The .NET resource files are designed to support such applications. In a nutshell, resource files can be set up for each culture being supported. For example, one file may have all the control labels and text on its interface in German; another may have the same controls with French text. When the application runs, it
373
374
Chapter 7
■
Windows Forms Controls
looks at the culture settings of the computer it is running on and pulls in the appropriate resources. This little bit of magic is accomplished by associating resource files with the CultureInfo class that designates a language, or language and culture. The resource files are packaged as satellite assemblies, which are resource files stored as DLLs.
Resource Localization Using Visual Studio.NET To make a form localized, you must set its Localizable property to true. This has the effect of turning each control on a form into a resource that has its properties stored in the form’s .resx file. This sets the stage for creating separate .resx files for each culture a form supports. Recall from Chapter 5, “C# Text Manipulation and File I/O,” that a culture is specified by a two-character language code followed by an optional two-character country code. For example, the code for English in the United States is en-US. The terms neutral culture and specific culture are terms to describe a culture. A specific culture has both the language and country specified; a neutral culture has only the language. Consult the MSDN documentation on the CultureInfo class for a complete list of culture names. To associate other cultures with a form, set the form’s Language property to another locale from the drop-down list in the Properties window. This causes a .resx file to be created for the new culture. You can now customize the form for this culture by changing text, resizing controls, or moving controls around. This new property information is stored in the .resx file for this culture only—leaving the .resx files for other cultures unaffected. The resource files are stored in folders, as shown in Figure 7-21. When the project is built, a satellite assembly is created to contain the resources for each culture, as shown in Figure 7-22. This DLL file has the same name in each folder.
Figure 7-21 VS.NET resource files for multiple cultures
Figure 7-22
Satellite assembly
7.9 Using Resources
Determining Localization Resources at Runtime By default, an application’s thread has its CurrentThread.CurrentUICulture property set to the culture setting of the machine it is running on. Instances of the ResourceManager, in turn, use this value to determine which resources to load. They do this by searching for the satellite assembly in the folder associated with the culture—a reason why the naming and location of resource folders and files is important. If no culture-specific resources are found, the resources in the main assembly are used.
Core Note The easiest way to test an application with other culture settings is to set the CurrentUICulture to the desired culture. The following statement, for example, is placed before InitializeComponent() in VS.NET to set the specific culture to German: System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-DE");
Creating a Satellite Assembly Without VS.NET One of the advantages of using satellite assemblies is that they can be added to an application, or modified, without recompiling the application. The only requirements are that a folder be set up along the proper path, and that the folder and satellite assembly have the proper name. Suppose you have a .resx file that has been converted by your translator to French Canadian. You can manually create and add a satellite assembly to the application in three steps: 1. Convert the.resx file to a .resources file: filmography.Form1.fr-CA.resources
2. Convert the .resources file to a satellite assembly using the Assembly Linker (Al.exe): Al.exe /t:lib /embed:filmography.Form1.fr-CA.resources /culture:fr-CA /out:filmography.resources.dll
3. Create the fr-CA folder beneath Release folder and copy the new assembly file into it.
375
376
Chapter 7
■
Windows Forms Controls
Placing the satellite assembly in the proper folder makes it immediately available to the executable and does not require compiling the application.
7.10 Summary There are more than 50 GUI controls available in the .NET Framework Class Library. This chapter has taken a selective look at some of the more important ones. They all derive from the System.Windows.Forms.Control class that provides the inherited properties and methods that all the controls have in common. Although each control is functionally unique, it is possible to create a taxonomy of controls based on similar characteristics and behavior. The button types, which are used to intitiate an action or make a selection, include the simple Button, CheckBox, and RadioButton. These are often grouped using a GroupBox or Panel control. The TextBox can be used to hold a single line of text or an entire document. Numerous methods are available to search the box and identify selected text within it. The PictureBox is available to hold images and has a SizeMode property that is used to position and size an image within the box. Several controls are available for presenting lists of data. The ListBox and ComboBox display data in a simple text format. However, the underlying data may be a class object with multiple properties. The TreeView and ListView are useful for displaying data with a hierarchical relationship. The ListView can display data in multiple views that include a grid layout and icon representation of data. The TreeView presents a tree metaphor to the developer, with data represented as parent and child nodes. Most of the controls support the drag-and-drop operation that makes it easy to move or copy data from one control to another. The source control initiates the action by calling a DoDragDrop method that passes the data and permissible effects to the target control. For applications that require nonstandard controls, .NET lets you create custom controls. They may be created from scratch, derived from an existing control, or created as a combination of controls in a user control container.
7.11 Test Your Understanding 1. Why is a container control such as a GroupBox used with radio buttons? 2. What is the SizeMode property set to in order to automatically resize and fill an image in a PictureBox?
7.11 Test Your Understanding
3. Suppose you place objects in a ListBox that have these properties: string Vendor, string ProductID, int Quantity
How do you have the ListBox display the ProductID and Quantity? 4. What event is fired when an item in a ListBox is selected? What ListBox properties are used to identify the selected item? 5. What property and value are set on a ListView to display its full contents in a grid layout? 6. Which TreeNode property can be used to store object data in a TreeView node? 7. Which two events must the destination control in a drag-and-drop operation support? 8. The Property Browser in VS.NET uses metadata to categorize a control’s properties and events and assign default values. How do you generate this information for the properties in a custom control? 9. What class is used to read text from a text resource file embedded in an assembly? What method is used to read values from the file?
377
.NET GRAPHICS USING GDI+
Topics in This Chapter • Graphics Overview: The first step in working with GDI+ is to understand how to create and use a Graphics object. This section looks at how this object is created and used to handle the Paint event. • Using the Graphics Object to Create Shapes: .NET offers a variety of standard geometric shapes that can be drawn in outline form or filled in. The GraphicsPath class serves as a container that enables geometric shapes to be connected. • Using Pens and Brushes: Pens are used to draw shapes in outline form in different colors and widths; a brush is used to fill in shapes and create solid and gradient patterns. • Color: Colors may be defined according to red/green/blue (RGB) values or hue/saturation/brightness (HSB) values. Our project example illustrates RGB and HSB color spaces. By representing a color as an object, .NET permits it to be transformed by changing property values. • Images: .NET includes methods to load, display, and transform images. The most useful of these is the Graphics.DrawImage method that allows images to be magnified, reduced, and rotated.
8
Very few programmers are artists, and only a minority of developers is involved in the world of gaming where graphics have an obvious justification. Yet, there is something compelling about writing an application that draws on a computer screen. For one thing, it’s not difficult. An array of built-in functions makes it easy to create geometric objects, color them, and even animate them. In this regard, .NET should satisfy the would-be artist that resides in many programmers. To understand the .NET graphics model, it is useful to look at its predecessor— the Win32 Graphical Device Interface (GDI). This API introduced a large set of drawing objects that could be used to create device independent graphics. The idea was to draw to a logical coordinate system rather than a device specific coordinate system—freeing the developer to concentrate on the program logic and not device details. .NET essentially takes this API, wraps it up in classes that make it easier to work with, and adds a wealth of new features. The graphics classes are collectively called GDI+. This chapter looks at the underlying principles that govern the use of the GDI+, and then examines the classes and the functionality they provide. Several programming examples are included that should provide the tools you will need to further explore the .NET graphics namespaces. Keep in mind that GDI+ is not restricted to WinForms applications. Its members are also available to applications that need to create images dynamically for the Internet (Web Forms and Web Services).You should also recognize that GDI+ is useful for more than just games or graphics applications. Knowledge of its classes is essential if you want to design your own controls or modify the appearance of existing ones. 379
380
Chapter 8
8.1
■
.NET Graphics Using GDI+
GDI+ Overview
The types that make up GDI+ are contained in the gdiplus.dll file. .NET neatly separates the classes and enumerations into logically named namespaces that reflect their use. As Figure 8-1 shows, the GDI+ functions fall into three broad categories: two-dimensional vector graphics, image manipulation, and typography (the combining of fonts and text strings to produce text output).
System.Drawing
Drawing2D
Imaging
Vector Graphics
Image Manipulation Figure 8-1
Printing
Text
Typography
GDI+ namespaces
This figure does not depict inheritance, but a general hierarchical relationship between the GDI+ namespaces. System.Drawing is placed at the top of the chart because it contains the basic objects required for any graphic output: Pen, Brush, Color, and Font. But most importantly, it contains the Graphics class. This class is an abstract representation of the surface or canvas on which you draw. The first requirement for any drawing operation is to get an instance of this class, so the Graphics object is clearly a fruitful place to begin the discussion of .NET graphics.
The Graphics Class Drawing requires a surface to draw on, a coordinate system for positioning and aligning shapes, and a tool to perform the drawing. GDI+ encapsulates this functionality in the System.Drawing.Graphics class. Its static methods allow a Graphics object to be created for images and controls; and its instance methods support the drawing of various shapes such as circles, triangles, and text. If you are familiar with using the Win32 API for graphics, you will recognize that this corresponds closely to a device context in GDI. But the Graphics object is of a simpler design. A device context is a structure that maintains state information about a drawing and is passed as an argument to drawing functions. The Graphics object represents the drawing surface and provides methods for drawing on it. Let’s see how code gains access to a Graphics object. Your application most likely will work with a Graphics object inside the scope of an event handler, where the
8.1 GDI+ Overview
object is passed as a member of an EventArgs parameter. The Paint event, which occurs each time a control is drawn, is by far the most common source of Graphics objects. Other events that have a Graphics object sent to their event handler include PaintValue, BeginPrint, EndPrint, and PrintDocument.PrintPage. The latter three are crucial to printing and are discussed in the next chapter. Although you cannot directly instantiate an object from the Graphics class, you can use methods provided by the Graphics and Control classes to create an object. The most frequently used is Control.CreateGraphics—an instance method that returns a graphics object for the control calling the method. The Graphics class includes the FromHwnd method that relies on passing a control’s Handle to obtain a Graphics object related to the control. Let’s look at both approaches.
How to Obtain a Graphics Object from a Control Using CreateGraphics The easiest way to create a Graphics object for a control is to use its CreateGraphics method. This method requires no parameters and is inherited from the Control class by all controls. To demonstrate, let’s create an example that draws on a Panel control when the top button is clicked and refreshes all or part of the panel in response to another button click. The user interface to this program is shown in Figure 8-2 and will be used in subsequent examples.
Figure 8-2 Interface to demonstrate using Graphics object to draw on a control
Listing 8-1 contains the code for the Click event handlers associated with each button. When the Decorate Panel button (btnDecor) is clicked, a Graphics object is created and used to draw a rectangle around the edge of the panel as well as a horizontal line through the middle. When the Refresh button (btnRefresh) is clicked, the panel’s Invalidate method is called to redraw all or half of the panel. (More on the Invalidate command is coming shortly.)
381
382
Chapter 8
■
Listing 8-1
.NET Graphics Using GDI+
Using Control.CreateGraphics to Obtain a Graphics Object
using System.Drawing; // private void btnDecor_Click(object sender, System.EventArgs e) { // Create a graphics object to draw on panel1 Graphics cg = this.panel1.CreateGraphics(); try ( int pWidth = panel1.ClientRectangle.Width; int pHeight = panel1.ClientRectangle.Height; // Draw a rectangle around border cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4); // Draw a horizontal line through the middle cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4, (pHeight-4)/2); } finally { cg.Dispose(); // You should always dispose of object } } private void btnRefresh_Click(object sender, System.EventArgs e) { // Invokes Invalidate to repaint the panel control if (this.radAll.Checked) // Radio button - All { // Redraw panel1 this.panel1.Invalidate(); } else { // Redraw left half of panel1 Rectangle r = new Rectangle(0,0,panel1.ClientRectangle.Width/2, ClientRectangle.Height); this.panel1.Invalidate(r); // Repaint area r this.panel1.Update(); // Force Paint event } }
The btnDecor Click event handler uses the DrawRectangle and DrawLine methods to adorn panel1. Their parameters—the coordinates that define the shapes—are derived from the dimensions of the containing panel control. When the drawing is completed, the Dispose method is used to clean up system resources
8.1 GDI+ Overview
held by the object. (Refer to Chapter 4, “Working with Objects in C#,” for a discussion of the IDisposable interface.) You should always dispose of the Graphics object when finished with it. The try-finally construct ensures that Dispose is called even if an interrupt occurs. As shown in the next example, a using statement provides an equivalent alternative to try-finally. The btnRefresh Click event handler is presented as a way to provide insight into how forms and controls are drawn and refreshed in a WinForms environment. A form and its child controls are drawn (displayed) in response to a Paint event. Each control has an associated method that is responsible for drawing the control when the event occurs. The Paint event is triggered when a form or control is uncovered, resized, or minimized and restored.
How to Obtain a Graphics Object Using Graphics Methods The Graphics class has three static methods that provide a way to obtain a Graphics object: •
•
•
Graphics.FromHdc. Creates the Graphics object from a specified
handle to a Win32 device context. This is used primarily for interoperating with GDI. Graphics.FromImage. Creates a Graphics object from an instance of a .NET graphic object such as a Bitmap or Image. It is often used in ASP.NET (Internet) applications to dynamically create images and graphs that can be served to a Web browser. This is done by creating an empty Bitmap object, obtaining a Graphics object using FromImage, drawing to the Bitmap, and then saving the Bitmap in one of the standard image formats. Graphics.FromHwnd. Creates the Graphics object from a handle to a Window, Form, or control. This is similar to GDI programming that requires a handle to a device context in order to display output to a specific device.
Each control inherits the Handle property from the Control class. This property can be used with the FromHwnd method as an alternative to the Control.CreateGraphics method. The following routine uses this approach to draw lines on panel1 when a MouseDown event occurs on the panel (see Figure 8-3). Note that the Graphics object is created inside a using statement. This statement generates the same code as a try-finally construct that includes a g.Dispose() statement in the finally block. private void panel1OnMouseDown(object sender, MouseEventArgs e) {
383
384
Chapter 8
■
.NET Graphics Using GDI+
// The using statement automatically calls g.Dispose() using( Graphics g= Graphics.FromHwnd(panel1.Handle)) { g.DrawLine(Pens.Red,e.X,e.Y,20,20); } }
Figure 8-3
Output for MouseDown example
The Paint Event A Paint event is triggered in a WinForms application when a form or control needs to be partially or fully redrawn. This normally occurs during the natural use of a GUI application as the window is moved, resized, and hidden behind other windows. Importantly, a Paint event can also be triggered programatically by a call to a control’s Invalidate method.
Using Invalidate() to Request a Paint Event The Control.Invalidate method triggers a Paint event request. The btnRefresh_Click event handler in Listing 8-1 showed two overloads of the method. The parameterless version requests that the entire panel control be redrawn; the second specifies that only the portion of the control’s region specified by a rectangle be redrawn. Here are some of the overloads for this method: public public public public
void void void void
Invalidate() Invalidate(bool invalidatechildren) Invalidate(Rectangle rc) Invalidate(Rectangle rc, bool invalidatechildren)
Note: Passing a true value for the invalidatechildren parameter causes all child controls to be redrawn. Invalidate requests a Paint event, but does not force one. It permits the operating system to take care of more important events before invoking the Paint event.
8.1 GDI+ Overview
To force immediate action on the paint request, follow the Invalidate statement with a call to Control.Update. Let’s look at what happens on panel1 after a Paint event occurs. Figure 8-4 shows the consequences of repainting the left half of the control. The results are probably not what you desire: half of the rectangle and line are now gone. This is because the control’s paint event handler knows only how to redraw the control. It has no knowledge of any drawing that may occur outside of its scope. An easy solution in this case is to call a method to redraw the rectangle and line after calling Invalidate. But what happens if Windows invokes the Paint event because half of the form is covered and uncovered by another window? This clears the control and our code is unaware it needs to redraw the rectangle and line. The solution is to handle the drawing within the Paint event handler.
Figure 8-4
Effects of invalidating a region
Core Note When a form is resized, regions within the original area are not redrawn. To force all of a control or form to be redrawn, pass the following arguments to its SetStyle method. Only use this when necessary, because it slows down the paint process. this.SetStyle(ControlStyles.ResizeRedraw, true);
Implementing a Paint Event Handler After the Paint event occurs, a data class PaintEventArgs is passed as a parameter to the Paint event handler. This class provides access to the Graphics object and to a rectangle ClipRectangle that defines the area where drawing may occur. Together, these properties make it a simple task to perform all the painting within the scope of the event handler. Let’s see how to rectify the problem in the preceding example, where our drawing on panel1 disappears each time the paint event occurs. The solution, of course, is to
385
386
Chapter 8
■
.NET Graphics Using GDI+
perform the drawing inside the paint event handler. To do this, first register our event handler with the PaintEventHandler delegate: this.panel1.Paint += new PaintEventHandler(paint_Panel);
Next, set up the event handler with the code to draw a rectangle and horizontal line on the panel. The Graphics object is made available through the PaintEventArgs parameter. private void paint_Panel( object sender, PaintEventArgs e) { Graphics cg = e.Graphics; int pWidth = panel1.ClientRectangle.Width; int pHeight = panel1.ClientRectangle.Height; cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4); cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4, (pHeight-4)/2); base.OnPaint(e); // Call base class implementation }
The Control.OnPaint method is called when a Paint event occurs. Its role is not to implement any functionality, but to invoke the delegates registered for the event. To ensure these delegates are called, you should normally invoke the OnPaint method within the event handler. The exception to this rule is: To avoid screen flickering, do not call this method if painting the entire surface of a control. Painting is a slow and expensive operation. For this reason, PaintEventArgs provides the ClipRectangle property to define the area that is displayed when drawing occurs. Any drawing outside this area is automatically clipped. However, it is important to realize that clipping affects what is displayed—it does not prevent the drawing code from being executed. Thus, if you have a time-consuming custom paint routine, the entire painting process will occur each time the routine is called, unless you include logic to paint only what is needed. The following example illustrates how to draw selectively. It paints a pattern of semi-randomly colored rectangles onto a form’s panel (see Figure 8-5). Before each rectangle is drawn, a check is made to confirm that the rectangle is in the clipping area. private void paint_Panel( object sender, PaintEventArgs e) { Graphics g = e.Graphics; for (int i = 0; i< this.panel1.Width;i+=20) { for (int j=0; j< his.panel1.Height;j+=20) { Rectangle r= new Rectangle(i,j,20,20); if (r.IntersectsWith(e.ClipRectangle))
8.1 GDI+ Overview
{ // FromArgb is discussed in Color section Brush b = new SolidBrush(Color.FromArgb((i*j)%255, (i+j)%255, ((i+j)*j)%255)); g.FillRectangle(b,r); g.DrawRectangle(Pens.White,r); } } } }
Figure 8-5 Pattern used in paint example
The key to this code is the Rectangle.IntersectsWith method that checks for the intersection of two rectangles. In this case, it tests for overlap between the rectangle to be drawn and the clip area. If the rectangle intersects the clip area, it needs to be drawn. Thus, the method can be used to limit the portion of the screen that has to be repainted. To test the effects, this code was run with and without the IntersectsWith method. When included, the event handler required 0 to 17 milliseconds—depending on the size of the area to be repainted. When run without IntersectsWith, the event handler required 17 milliseconds to redraw all the rectangles. Another approach to providing custom painting for a form or control is to create a subclass that overrides the base class’s OnPaint method. In this example, myPanel is derived from the Panel class and overrides the OnPaint method to draw a custom diagonal line through the center of the panel. // New class myPanel inherits from base class Panel public class myPanel: Panel {
387
388
Chapter 8
■
.NET Graphics Using GDI+
protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.DrawLine(Pens.Aqua,0,0,this.Width,this.Height); base.OnPaint(e); } }
Unless the new subclass is added to a class library for use in other applications, it is simpler to write an event handler to provide custom painting.
8.2
Using the Graphics Object
Let’s look at the details of actually drawing with the Graphics object. The Graphics class contains several methods for rendering basic geometric patterns. In addition to the DrawLine method used in previous examples, there are methods for drawing rectangles, polygons, ellipses, curves, and other basic shapes. In general, each shape can be drawn as an outline using the Pen class, or filled in using the Brush class. The DrawEllipse and FillEllipse methods are examples of this. Let’s look at some examples.
Basic 2-D Graphics Figure 8-6 demonstrates some of the basic shapes that can be drawn with the Graphics methods. The shapes vary, but the syntax for each method is quite similar. Each accepts a drawing object—either a pen or brush—to use for rendering the shape, and the coordinates that determine the size and positions of the shape. A Rectangle object is often used to provide a shape’s boundary.
DrawEllipse()
FillEllipse()
DrawPolygon() DrawPie() DrawRectangle() DrawArc()
FillPolygon()
Figure 8-6
FillPie()
DrawRectangle() Bevel
Basic 2-D shapes
FillArc()
8.2 Using the Graphics Object
The following code segments draw the shapes shown in Figure 8-6. To keep things simple, the variables x and y are used to specify the location where the shape is drawn. These are set to the coordinates of the upper-left corner of a shape. Pen blkPen = new Pen(Brushes.Black,2 ); // width=2 // Set this to draw smooth lines g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // (1) Draw Circle and Draw Filled Circle Rectangle r = new Rectangle(new Point(x,y),new Size(40,40)); g.DrawEllipse(blkPen, r); g.FillEllipse(Brushes.Black,x,y+60,40,40); // (2) Draw Ellipse and Filled Ellipse int w = 60; int h= 40; Rectangle r = new Rectangle(new Point(x,y),new Size(w,h)); g.DrawEllipse(blkPen, r); r = new Rectangle(new Point(x,y+60), new Size(w,h)); g.FillEllipse(Brushes.Red, r); // (3) Draw Polygon Point pt1 = new Point(x, y); Point pt2 = new Point(x+22, y+12); Point pt3 = new Point(x+22, y+32); Point pt4 = new Point(x, y+44); Point pt5 = new Point(x-22, y+32); Point pt6 = new Point(x-22, y+12); Point[] myPoints = {pt1, pt2, pt3, pt4, pt5, pt6}; g.DrawPolygon(blkPen, myPoints); // Points would be changed so as not to draw over // original polygon g.FillPolygon(Brushes.Black, myPoints); // (4)Draw Pie Shape and filled pie Rectangle r = new Rectangle( new Point(x,y),new Size(80,80)); // Create start and sweep angles int startAngle = 0; // Clockwise from x-axis int sweepAngle = -60; // Clockwise from start angle g.DrawPie(blkPen, r, startAngle, sweepAngle); g.FillPie(Brushes.Black, x,y+60,80,80,startAngle, sweepAngle); // (5) Draw Rectangle and Rectangle with beveled edges blkPen.Width=5; // make pen thicker to show bevel g.DrawRectangle(blkPen,x,y,50,40); blkPen.LineJoin = LineJoin.Bevel; g.DrawRectangle(blkPen,x,y+60,50,40); // (6) Draw Arc and Filled Pie startAngle=45; sweepAngle=180; g.DrawArc(blkPen, x,y,40,40,startAngle, sweepAngle); g.FillPie(Brushes.Black, x,y+60,40,40,startAngle,sweepAngle);
389
390
Chapter 8
■
.NET Graphics Using GDI+
These code segments illustrate how easy it is to create simple shapes with a minimum of code. .NET also makes it easy to create more complex shapes by combining primitive shapes using the GraphicsPath class.
Creating Shapes with the GraphicsPath Class The GraphicsPath class, which is a member of the System.Drawing.Drawing2D namespace, is used to create a container for a collection of primitive shapes. Succinctly, it permits you to add basic shapes to its collection and then to treat the collection as a single entity for the purpose of drawing and filling the overall shape. Before looking at a code example, you should be aware of some of the basic features of the GraphicsPath class: • •
• •
It automatically connects the last point of a line or arc to the first point of a succeeding line or arc. Its CloseFigure method can be used to automatically close open shapes, such as an arc. The first and last points of the shape are connected. Its StartFigure method prevents the previous line from being automatically connected to the next line. Its Dispose method should always be called when the object is no longer in use.
The following code creates and displays the Infinity Cross shown in Figure 8-7. It is constructed by adding five polygons to the GraphicsPath container object. The Graphics object then draws the outline and fills in the cross. // g is the Graphics object g.SmoothingMode = SmoothingMode.AntiAlias; // Define five polygons Point[] ptsT= {new Point(120,20),new Point(160,20), new Point(140,50)}; Point[] ptsL= {new Point(90,50),new Point(90,90), new Point(120,70)}; Point[] ptsB= {new Point(120,120),new Point(160,120), new Point(140,90)}; Point[] ptsR= {new Point(190,90), new Point(190,50), new Point(160, 70)}; Point[] ptsCenter = {new Point(140,50), new Point(120,70), new Point(140,90), new Point(160,70)}; // Create the GraphicsPath object and add the polygons to it GraphicsPath gp = new GraphicsPath(); gp.AddPolygon(ptsT); // Add top polygon gp.AddPolygon(ptsL); // Add left polygon gp.AddPolygon(ptsB); // Add bottom polygon
8.2 Using the Graphics Object
gp.AddPolygon(ptsR); // Add right polygon gp.AddPolygon(ptsCenter); g.DrawPath(new Pen(Color.Red,2),gp); // Draw GraphicsPath g.FillPath(Brushes.Gold,gp); // Fill the polygons
Figure 8-7
Infinity Cross
Instead of drawing and filling each polygon separately, we use a single DrawPath and FillPath statement to do the job. The GraphicsPath class has several methods worth exploring—AddCircle, AddArc, AddEllipse, AddString, Warp, and others—for applications that require the complex manipulation of shapes. One of the more interesting is the Transform method that can be used to rotate or shift the coordinates of a DrawPath object. This following code segment offers a taste of how it works. A transformation matrix is created with values that shift the x coordinates by 50 units and leave the y coordinates unchanged. This Transform method applies the matrix to the DrawPath and shifts the coordinates; the shape is then drawn 50 units to the right of the first shape. Matrix translateMatrix = new Matrix(); translateMatrix.Translate(50, 0); // Offset x coordinate by 50 gp.Transform(translateMatrix); // Transform path g.DrawPath(Pens.Orange,gp); // Display at new location
Hit Testing with Shapes One of the reasons for placing shapes on a form is to permit a user to trigger an action by clicking a shape—as if she had clicked a button. Unlike a control, you cannot associate an event with a shape. Instead, you associate a MouseDown event with the container that holds the shape(s). Recall that a MouseDown event handler receives the x and y coordinates where the event occurs. After it has these, it is a simple process to use the rectangle and GraphicsPath methods to verify whether a point falls within their area: bool Rectangle.Contains(Point(x,y)) bool GraphicsPath.IsVisible(Point(x,y))
391
392
Chapter 8
■
.NET Graphics Using GDI+
To illustrate, consider an application that displays a map of US states and responds to a click on a state by displaying the name of the state capital. The map image is placed in a PictureBox, and rectangles and polygons are drawn on the states to set up the hit areas that respond to a MouseDown event. Figure 8-8 shows how the picture box’s paint handler routine draws rectangles on three states and a polygon on Florida. (Of course, the shapes would not be visible in the actual application.) To respond to a pressed mouse key, set up a delegate to call an event handler when the MouseDown event occurs: this.pictureBox1.MouseDown += new MouseEventHandler(down_Picture);
Figure 8-8
Hit test example
The following code implements event handler logic to determine if the mouse down occurs within the boundary of any shape. private void down_Picture( object sender, MouseEventArgs e) { // Rectangles and GraphicsPath gp are defined // as class variables if (rectNC.Contains(e.X,e.Y) ) { MessageBox.Show("Capital: Raleigh"); } else if(rectSC.Contains(e.X,e.Y)) { MessageBox.Show("Capital: Columbia");} else if(rectGA.Contains(e.X,e.Y)) { MessageBox.Show("Capital: Atlanta");} else if(gp.IsVisible(e.X,e.Y)) {MessageBox.Show("Capital: Tallahassee");} }
8.2 Using the Graphics Object
After you have a basic understanding of how to create and use shapes, the next step is to enhance these shapes with eye catching graphical effects such as gradients, textured colors, and different line styles and widths. This requires an understanding of the System.Drawing classes: Pen, Brush, and Color.
Pens The Graphics object must receive an instance of the Pen class to draw a shape’s outline. Our examples thus far have used a static property of the Pens class— Pens.Blue, for example—to create a Pen object that is passed to the Graphics object. This is convenient, but in many cases you will want to create your own Pen object in order to use non-standard colors and take advantage of the Pen properties. Constructors: public public public public
Pen Pen Pen Pen
(Color (Color (Brush (Brush
color); color, single width); brush); brush, single width);
Example: Pen p1 = new Pen(Color.Red, 5); Pen p2 = new Pen(Color.Red); // Default width of 1
The constructors allow you to create a Pen object of a specified color and width. You can also set its attributes based on a Brush object, which we cover later in this section. Note that the Pen class inherits the IDisposable interface, which means that you should always call the Pen object’s Dispose method when finished with it. Besides color and width, the Pen class offers a variety of properties that allow you to control the appearance of the lines and curves the Pen object draws. Table 8-1 contains a partial list of these properties. Table 8-1 Selected Pen Properties Member
Description
Alignment
Determines how a line is drawn for closed shapes. Specifically, it specifies whether the line is drawn on the bounding perimeter or inside it.
Color
Color used to draw the shape or text.
393
394
Chapter 8
■
.NET Graphics Using GDI+
Table 8-1 Selected Pen Properties (continued) Member
Description
DashCap
The cap style used at the beginning and end of dashes in a dashed line. A cap style is a graphic shape such as an arrow.
DashOffset
Distance from start of a line to the beginning of its dash pattern.
DashStyle
The type of dashed lines used. This is based on the DashStyle enumeration.
PenType
Specifies how a line is filled—for example, textured, solid, or gradient. It is determined by the Brush property of the Pen.
StartCap EndCap
The cap style used at the beginning and end of lines. This comes from the LineCap enumeration that includes arrows, diamonds, and squares—for example, LineCap.Square.
Width
Floating point value used to set width of Pen.
Let’s look at some of the more interesting properties in detail.
DashStyle This property defines the line style, which can be Solid, Dash, Dot, DashDot, DashDotDot, or Custom (see Figure 8-9). The property’s value comes from the DashStyle enumeration. Pen p1 = new Pen(Color.Black, 3); p1.DashStyle = DashStyle.Dash; g.DrawLine(p1,20,20,180,20);
Dash DashDot DashDotDot Dot Solid
Figure 8-9 DashStyles and LineCaps
8.2 Using the Graphics Object
StartCap and EndCap These properties define the shape used to begin and end a line. The value comes from the LineCap enumeration, which includes ArrowAnchor, DiamondAnchor, Round, RoundAnchor, Square, SquareAnchor, and Triangle. Examples of the DiamondAnchor and RoundAnchor are shown in Figure 8-9. The following code is used to create the lines in the figure: Graphics g = pictureBox1.CreateGraphics(); Pen p1 = new Pen(Color.Black, 5); p1.StartCap = LineCap.DiamondAnchor; p1.EndCap = LineCap.RoundAnchor; int yLine = 20; foreach(string ds in Enum.GetNames(typeof(DashStyle))) { if (ds != "Custom") // Ignore Custom DashStyle type { // Parse creates an enum type from a string p1.DashStyle = (DashStyle)Enum.Parse( typeof(DashStyle), ds); g.DrawLine(p1,20,yLine,120,yLine); g.DrawString(ds,new Font("Arial",10),Brushes.Black, 140,yLine-8); yLine += 20; } }
The code loops through the DashStyle enumeration and draws a line for each enum value except Custom. It also uses the DrawString method to display the name of the enumeration values. This method is discussed in Chapter 9.
Brushes Brush objects are used by these Graphics methods to create filled geometric shapes: FillClosedCurve FillPolygon
FillEllipse FillRectangle
FillPath FillRectangles
FillPie FillRegion
All of these receive a Brush object as their first argument. As with the Pen class, the easiest way to provide a brush is to use a predefined object that represents one of the standard colors—for example, Brushes.AntiqueWhite. To create more interesting effects, such as fills with patterns and gradients, it is necessary to instantiate your own Brush type. Unlike the Pen class, you cannot create an instance of the abstract Brush class; instead, you use one of its inheriting classes summarized in Table 8-2.
395
396
Chapter 8
■
.NET Graphics Using GDI+
Table 8-2 Brush Types That Derive from the Brush Class Brush Type
Description
SolidBrush
Defines a brush of a single color. It has a single constructor: Brush b = new SolidBrush(Color.Red);
TextureBrush
Uses a preexisting image (*.gif, *.bmp, or *.jpg) to fill a shape. Image img = Image.FromFile("c:\\flower.jpg"); Brush b = new TextureBrush(img);
HatchBrush
Defines a rectangular brush with a foreground color, background color, and hatch style. Located in the System.Drawing.Drawing2D namespace.
LinearGradientBrush
Supports either a two-color or multi-color gradient. All linear gradients occur along a line defined by two points or a rectangle. Located in the Drawing2D namespace.
PathGradientBrush
Fills the interior of a GraphicsPath object with a gradient. Located in the Drawing2D namespace.
Note that all Brush classes have a Dispose method that should be called to destroy the Brush object when it is no longer needed. The two most popular of these classes are HatchBrush, which is handy for creating charts, and LinearGradientBrush, for customizing the background of controls. Let’s take a closer look at both of these.
The HatchBrush Class As the name implies, this class fills the interior of a shape with a hatched appearance. Constructors: public HatchBrush(HatchStyle hStyle, Color forecolor) public HatchBrush(HatchStyle hstyle, Color forecolor, Color backcolor)
Parameters: hStyle
HatchStyle enumeration that specifies the hatch pattern.
forecolor
The color of the lines that are drawn.
backcolor
Color of the space between the lines (black is default).
8.2 Using the Graphics Object
The predefined HatchStyle patterns make it a simple process to create elaborate, multi-color fill patterns. The following code is used to create the DarkVertical and DottedDiamond rectangles at the top of each column in Figure 8-10. Graphics g = pictureBox1.CreateGraphics(); // Fill Rectangle with DarkVertical pattern Brush b = new HatchBrush(HatchStyle.DarkVertical, Color.Blue,Color.LightGray); g.FillRectangle(b,20,20,80,60); // Fill Rectangle with DottedDiamond pattern b = new HatchBrush(HatchStyle.DottedDiamond, Color.Blue,Color.LightGray); g.FillRectangle(b,120,20,80,60);
DarkVertical
DottedDiamond
Cross
Weave
SmallGrid
Percent10
Figure 8-10 Using HatchBrush with some of the available hatch styles
The LinearGradientBrush Class In its simplest form, this class creates a transition that takes one color and gradually blends it into a second color. The direction of the transition can be set to horizontal, vertical, or any specified angle. The location where the transition begins can be set to a focal point other than the beginning of the area to be filled in. In cases where the gradient must be tiled to completely fill an area, options are available to control how each repeat is displayed. These options can be confusing, so let’s begin with how to
397
398
Chapter 8
■
.NET Graphics Using GDI+
create a gradient brush and then work with examples that demonstrate the more useful properties and methods of the LinearGradientBrush class. Constructors: public LinearGradientBrush(Rectangle rect, Color color1, Color color2, LinearGradientMode linearGradientMode) public LinearGradientBrush(Rectangle rect, Color color1, Color color2, float angle)
Parameters: rect
Rectangle specifying the bounds of the gradient.
color1
The start color in the gradient.
color2
The end color in the gradient.
angle
The angle in degrees moving clockwise from the x axis.
LinearGradientMode A LinearGradientMode enum value: Horizontal, Vertical, BackwardDiagonal, ForwardDiagonal
There is no substitute for experimentation when it comes to understanding graphics related concepts. Figure 8-11 shows the output from filling a rectangle with various configurations of a LinearGradientBrush object. Here is the code that creates these examples: // Draw rectangles filled with gradient in a pictureBox Graphics g = pictureBox1.CreateGraphics(); Size sz = new Size(100,80); Rectangle rb = new Rectangle(new Point(20,20),sz); // (1) Vertical Gradient (90 degrees) LinearGradientBrush b = new LinearGradientBrush(rb,Color.DarkBlue,Color.LightBlue,90); g.FillRectangle(b,rb); rb.X=140; // (2) Horizontal Gradient b = new LinearGradientBrush(rb,Color.DarkBlue, Color.LightBlue,0); g.FillRectangle(b,rb); rb.Y = 120; rb.X = 20; // (3) Horizontal with center focal point b = new LinearGradientBrush(rb,Color.DarkBlue, Color.LightBlue,0); // Place end color at position (0-1) within brush
8.2 Using the Graphics Object
b.SetBlendTriangularShape(.5f); g.FillRectangle(b,rb);
1
2
3
4
Figure 8-11 LinearGradientBrush examples: (1) Vertical, (2) Horizontal, (3) Focus Point, (4) Tiling
The main point of interest in this code is the use of the SetBlendTriangularShape method to create the blending effect shown in the third rectangle in Figure 8-11. This method takes an argument between 0 and 1.0 that specifies a relative focus point where the end color is displayed. The gradient then “falls off” on either side of this point to the start color. The fourth rectangle in the figure is created by repeating the original brush pattern. The following code defines a small gradient brush that is used to fill a larger rectangle: // Tiling Example – create small rectangle for gradient brush Rectangle rb1 = new Rectangle(new Point(0,0),new Size(20,20)); b = new LinearGradientBrush(rb,Color.DarkBlue, Color.LightBlue,0); b.WrapMode = WrapMode.TileFlipX; // Fill larger rectangle with repeats of small gradient rectangle g.FillRectangle(b,rb);
Notice how the light and dark colors are reversed horizontally before each repeat occurs: [light-dark][dark-light]. The WrapMode property determines how the repeated gradient is displayed. In this example, it is set to the WrapMode enum value of TileFlipX, which causes the gradient to be reversed horizontally before repeating. The most useful enum values include the following: Tile
Repeats the gradient.
TileFlipX
Reverses the gradient horizontally before repeating.
TileFlipXY
Reverses the gradient horizontally and vertically before repeating.
TileFlipY
Reverses the gradient vertically before repeating.
399
400
Chapter 8
■
.NET Graphics Using GDI+
Creating a Multi-Color Gradient It takes only a few lines of code to create a LinearGradientBrush object that creates a multi-color gradient. The key is to set the LinearGradientBrush.InterpolationColors property to an instance of a ColorBlend class that specifies the colors to be used. As the following code shows, the ColorBlend class contains an array of colors and an array of values that indicate the relative position (0–1) of each color on the gradient line. This example creates a gradient with a transition from red to white to blue—with white in the middle. b = new LinearGradientBrush(rb,Color.Empty,Color.Empty,0); ColorBlend myBlend = new ColorBlend(); // Specify colors to include in gradient myBlend.Colors = new Color[] {Color.Red, Color.White, Color.Blue,}; // Position of colors in gradient myBlend.Positions = new float[] {0f, .5f, 1f}; b.InterpolationColors = myBlend; // Overrides constructor colors
Colors .NET implements the Color object as a structure that includes a large number of colors predefined as static properties. For example, when a reference is made to Color.Indigo, the returned value is simply the Indigo property. However, there is more to the structure than just a list of color properties. Other properties and methods permit you to deconstruct a color value into its internal byte representation or build a color from numeric values. To appreciate this, let’s look at how colors are represented. Computers—as opposed to the world of printing—use the RGB (red/green/blue) color system to create a 32-bit unsigned integer value that represents a color. Think of RGB as a three-dimensional space with the red, green, and blue values along each axis. Any point within that space represents a unique RGB coordinate value. Throw in a fourth component, the alpha value—that specifies the color’s transparency—and you have the 4-byte AlphaRGB (ARGB) value that defines a color. For example, Indigo has RGB values of 75, 0, 130, and an alpha value of 255 (no transparency). This is represented by the hex value 4B0082FF. Colors can also be represented by the HSL (hue/saturation/luminosity) and HSB (hue/saturation/brightness) color spaces. While RGB values follow no easily discernible pattern, HSL and HSB are based on the standard color wheel (see Figure 8-12) that presents colors in an orderly progression that makes it easy to visualize the sequence. Hue is represented as an angle going counterclockwise around the wheel. The saturation is the distance from the center of the wheel toward the outer edge. Colors on the outer edge have full saturation. Brightness measures the intensity of a color. Colors shown on the wheel have 100% brightness, which decreases as they are
8.2 Using the Graphics Object
darkened (black has 0% brightness). There is no standard for assigning HSB/HSL values. Programs often use values between 0 and 255 to correspond to the RGB numbering scheme. As we will see, .NET assigns the actual angle to the hue, and values between 0 and 1 to the saturation and brightness. yellow 60˚
green 120˚
180˚ cyan
0˚ red
300˚ magenta
240˚ blue Figure 8-12
Color wheel
How to Create a Color Object The Color structure provides three static methods for creating a Color object: FromName, FromKnownColor, and FromArgb. FromName takes the name of a color as a string argument and creates a new struct: Color magenta = Color.FromName("Magenta"). The name must match one in the KnownColor enumeration values, which is an enumeration of all the colors represented as properties in the Color and SystemColor structures. FromKnownColor takes a KnownColor enumeration value as its argument and produces a struct for that color: Color magenta = Color.FromKnownColor(KnownColor.Magenta); FromArgb allows you to specify a color by RGB and alpha values, which makes it easy to change the transparency of an existing color. Here are some of its overloads: // (r, g, b) Color slate1 = Color.FromArgb (112, 128, 144); // (alpha, r, g, b) Color slate2 = Color.FromArgb (255, 112, 128, 144); // (alpha, Color) Color lightslate = Color.FromArgb(50, slate2 );
401
402
Chapter 8
■
.NET Graphics Using GDI+
Examining the Characteristics of a Color Object The Color structure has four properties that return the ARGB values of a color: Color.A, Color.R, Color.G, and Color.B. All of these properties have a value in the range 0 to 255. Color slateGray = Color.FromArgb(255,112,128,144); byte a = slateGray.A; // 255 byte r = slateGray.R; // 112 byte g = slateGray.G; // 128 byte b = slateGray.B; // 144
The individual HSB values of a color can be extracted using the Color.GetHue, GetSaturation, and GetBrightness methods. The hue is measured in degrees as a value from 0.0 to 360.0. Saturation and brightness have values between 0 and 1. Color float float float
slateGray = Color.FromArgb(255,112,128,144); hue = slateGray.GetHue(); // 210 degrees sat = slateGray.GetSaturation(); // .125 brt = slateGray.GetBrightness(); // .501
Observe in Figure 8-12 that the hue of 210 degrees (moving clockwise from 0) falls between cyan and blue on the circle—which is where you would expect to find a slate gray color.
A Sample Project: Building a Color Viewer The best way to grasp how the color spaces relate to actual .NET colors is to visually associate the colors with their RGB and HSB values. .NET offers a ColorDialog class that can be used to display available colors; however, it does not identify colors by the system-defined names that most developers work with. So, let’s build a simple color viewer that displays colors by name and at the same time demonstrates how to use the GDI+ types discussed in this section. Figure 8-13 shows the user interface for this application. It consists of a TreeView on the right that contains all the colors in the KnownColor enumeration organized into 12 color groups. These groups correspond to the primary, secondary, and tertiary1 colors of the color wheel. In terms of hues, each section is 30 degrees on the circle. The interface also contains two panels, a larger one in which a selected color is displayed, and a smaller one that displays a brightness gradient for the selected color. This is created using a multi-color gradient comprising black and white at each end, and the color at a focus point determined by its Brightness value. The remainder 1. Tertiary colors are red-orange, yellow-orange, yellow-green, blue-green, blue-violet, and red-violet.
8.2 Using the Graphics Object
of the screen displays RGB and HSB values obtained using the properties and methods discussed earlier.
Figure 8-13
Example: Color viewer demonstrates working with colors and gradients
The code for this application is shown in Listings 8-2 and 8-3. The former contains code to populate the TreeNode structure with the color nodes; Listing 8-3 shows the methods used to display the selected color and its color space values. The routine code for laying out controls on a form is excluded.
Listing 8-2
Color Viewer: Populate TreeNode with All Colors
using System.Drawing; using System.Windows.Forms; using System.Drawing.Drawing2D; public class Form1 : Form { public Form1() { InitializeComponent(); // Lay out controls on Form1 // Set up event handler to be fired when node is selected colorTree.AfterSelect += new TreeViewEventHandler(ColorTree_AfterSelect); BuildWheel(); // Create Parent Nodes for 12 categories }
403
404
Chapter 8
■
Listing 8-2
.NET Graphics Using GDI+
Color Viewer: Populate TreeNode with All Colors (continued)
[STAThread] static void Main() { Application.Run(new Form1()); } // Parent Nodes in TreeView for each segment of color wheel private void BuildWheel() { TreeNode tNode; tNode = colorTree.Nodes.Add("Red"); tNode = colorTree.Nodes.Add("Orange"); tNode = colorTree.Nodes.Add("Yellow"); // Remainder of nodes are added here .... } private void button1_Click(object sender, System.EventArgs e) { // Add Colors to TreeNode Structure // Loop through KnownColor enum values Array objColor = Enum.GetValues(typeof(KnownColor)); for (int i=0; i < objColor.Length; i++) { KnownColor kc = (KnownColor) objColor.GetValue(i); Color c = Color.FromKnownColor(kc); if (!c.IsSystemColor) // Exclude System UI colors { InsertColor(c, c.GetHue()); } } } private void InsertColor(Color myColor, float hue) { TreeNode tNode; TreeNode cNode = new TreeNode(); // Offset is used to start color categories at 345 degrees float hueOffset = hue + 15; if (hueOffset >360) hueOffset -= 360; // Get one of 12 color categories int colorCat = (int)(hueOffset -.1)/30; tNode = colorTree.Nodes[colorCat]; // Get parent node // Add HSB values to node's Tag HSB nodeHSB = new HSB(hue, myColor.GetSaturation(), myColor.GetBrightness());
8.2 Using the Graphics Object
Listing 8-2
Color Viewer: Populate TreeNode with All Colors (continued)
cNode.Tag = nodeHSB; // Tag contains HSB values cNode.Text = myColor.Name; int nodeCt = tNode.Nodes.Count; bool insert=false; // Insert colors in ascending hue value for (int i=0; i< nodeCt && insert==false ;i++) { nodeHSB = (HSB)tNode.Nodes[i].Tag; if (hue < nodeHSB.Hue) { tNode.Nodes.Insert(i,cNode); insert = true; } } if (!insert) tNode.Nodes.Add(cNode); } public struct HSB { public float Hue; public float Saturation; public float Brightness; public HSB(float H, float S, float B) { Hue = H; Saturation = S; Brightness = B; } } // ---> Methods to Display Colors go here }
When the application is executed, the form’s constructor calls BuildWheel to create the tree structure of parent nodes that represent the 12 color categories. Then, when the Build Color Tree button is clicked, the Click event handler loops through the KnownColor enum value (excluding system colors) and calls InsertColor to insert the color under the correct parent node. The Tag field of the added node (color) is set to an HSB struct that contains the hue, saturation, and brightness for the color. Nodes are stored in ascending order of Hue value. (See Chapter 7, “Windows Forms Controls,” for a discussion of the TreeNode control.)
405
406
Chapter 8
■
.NET Graphics Using GDI+
Listing 8-3 contains the code for both the node selection event handler and for ShowColor, the method that displays the color, draws the brightness scale, and fills
all the text boxes with RGB and HSB values.
Listing 8-3
Color Viewer: Display Selected Color and Values
private void ColorTree_AfterSelect(Object sender, TreeViewEventArgs e) // Event handler for AfterSelect event { // Call method to display color and info if (e.Node.Parent != null) ShowColor(e.Node); } private void ShowColor(TreeNode viewNode) { Graphics g = panel1.CreateGraphics(); // Color panel Graphics g2 = panel2.CreateGraphics(); // Brightness panel try { // Convert node's text value to Color object Color myColor = Color.FromName(viewNode.Text); Brush b = new SolidBrush(myColor); // Display selected color g.FillRectangle(b, 0,0,panel1.Width,panel1.Height); HSB hsbVal= (HSB) viewNode.Tag; // Convert hue to value between 0 and 255 for displaying int huescaled = (int) (hsbVal.Hue / 360 * 255); hText.Text = huescaled.ToString(); sText.Text = hsbVal.Saturation.ToString(); lText.Text = hsbVal.Brightness.ToString(); rText.Text = myColor.R.ToString(); gText.Text = myColor.G.ToString(); bText.Text = myColor.B.ToString(); // Draw Brightness scale Rectangle rect = new Rectangle(new Point(0,0), new Size(panel2.Width, panel2.Height)); // Create multi-color brush gradient for brightness scale LinearGradientBrush bg = new LinearGradientBrush(rect, Color.Empty, Color.Empty,90); ColorBlend myBlend = new ColorBlend(); myBlend.Colors = new Color[] {Color.White, myColor, Color.Black}; myBlend.Positions = new float[]{0f, 1-hsbVal.Brightness,1f};
8.3 Images
Listing 8-3
Color Viewer: Display Selected Color and Values (continued)
bg.InterpolationColors = myBlend; g2.FillRectangle(bg, rect); // Draw marker on brightness scale showing current color int colorPt = (int)((1–hsbVal.Brightness)* panel1.Height); g2.FillRectangle(Brushes.White,0,colorPt,10,2); b.Dispose(); bg.Dispose(); } finally { g.Dispose(); g2.Dispose(); } }
The code incorporates several of the concepts already discussed in this section. Its main purpose is to demonstrate how Color, Graphics, and Brush objects work together. A SolidBrush is used to fill panel1 with a color sample, a gradient brush creates the brightness scale, and Color properties provide the RGB values displayed on the screen.
8.3
Images
GDI+ provides a wide range of functionality for working with images in a runtime environment. It includes support for the following: • •
•
The standard image formats such as GIF and JPG files. Creating images dynamically in memory that can then be displayed in a WinForms environment or served up as images on a Web server or Web Service. Using images as a surface to draw on.
The two most important classes for handling images are the Image and Bitmap class. Image is an abstract class that serves as a base class for the derived Bitmap class. It provides useful methods for loading and storing images, as well as gleaning information about an image, such as its height and width. But for the most part, working with
407
408
Chapter 8
■
.NET Graphics Using GDI+
images requires the creation of objects that represent raster images. This responsibility devolves to the Bitmap class, and we use it exclusively in this section. Tasks associated with using images in applications fall into three general categories: •
•
•
Loading and storing images. Images can be retrieved from files, from a stream of bytes, from the system clipboard, or from resource files. After you have loaded or created an image, it is easily saved into a specified image format using the Save method of the Image or Bitmap class. Displaying an image. Images are dynamically displayed by writing them to the surface area of a form or control encapsulated by a Graphics object. Manipulating an image. An image is represented by an array of bits in memory. These bits can be manipulated in an unlimited number of ways to transform the image. Traditional image operations include resizing, cropping, rotating, and skewing. GDI+ also supports changing an image’s overall transparency or resolution and altering individual bits within an image.
Loading and Storing Images The easiest way to bring an image into memory is to pass the name of the file to a Bitmap constructor. Alternatively, you can use the FromFile method inherited from the Image class. string fname = "c:\\globe.gif"; Bitmap bmp = new Bitmap(fname); bmp = (Bitmap)Bitmap.FromFile(fname); // Cast to convert Image
In both cases, the image stored in bmp is the same size as the image in the file. Another Bitmap constructor can be used to scale the image as it is loaded. This code loads and scales an image to half its size: int w = Image.FromFile(fname).Width; int h = Image.FromFile(fname).Height; Size sz= new Size(w/2,h/2); bmp = new Bitmap(Image.FromFile(fname), sz); //Scales
GDI+ support images in several standard formats: bitmaps (BMP), Graphics Interchange Format (GIF), Joint Photographic Experts Group (JPEG), Portable Network Graphics (PNG), and the Tag Image File Format (TIFF). These are used for both loading and storing images and, in fact, make it quite simple to convert one format to another. Here is an example that loads a GIF file and stores it in JPEG format:
8.3 Images
string fname = "c:\\globe.gif"; bmp = new Bitmap(Image.FromFile(fname)); bmp.Save("c:\\globe.jpg", System.Drawing.Imaging.ImageFormat.Jpeg); // Compare size of old and new file FileInfo fi= new FileInfo(fname); int old = (int) fi.Length; fi = new FileInfo("c:\\globe.jpg"); string msg = String.Format("Original: {0} New: {1}",old,fi.Length); MessageBox.Show(msg); // ---> Original: 28996 New: 6736
The Save method has five overloads; its simplest forms take the name of the file to be written to as its first parameter and an optional ImageFormat type as its second. The ImageFormat class has several properties that specify the format of the image output file. If you have any experience with image files, you already know that the format plays a key role in the size of the file. In this example, the new JPEG file is less than one-fourth the size of the original GIF file. To support multiple file formats, GDI+ uses encoders to save images to a file and decoders to load images. These are referred to generically as codecs (code-decode). An advantage of using codecs is that new image formats can be supported by writing a decoder and encoder for them. .NET provides the ImageCodecInfo class to provide information about installed image codecs. Most applications allow GDI+ to control all aspects of loading and storing image files, and have no need for the codecs information. However, you may want to use it to discover what codecs are available on your machine. The following code loops through and displays the list of installed encoders (see Figure 8-14): // Using System.Drawing.Imaging string myList=""; foreach(ImageCodecInfo co in ImageCodecInfo.GetImageEncoders()) myList = myList +"\n"+co.CodecName; Console.WriteLine(myList);
Figure 8-14 Codecs
409
410
Chapter 8
■
.NET Graphics Using GDI+
The DrawImage method of the Graphics object is used to display an image on the Graphics object’s surface. These two statements load an image and draw it full size at the upper-left corner of the graphics surface (0,0). If the Graphics object surface is smaller than the image, the image is cropped (see the first figure in the following example). Bitmap bmp = new Bitmap("C:\\globe.gif"); g.DrawImage(bmp,0,0); // Draw at coordinates 0,0 DrawImage has some 30 overloaded versions that give you a range of control over sizing, placement, and image selection. Many of these include a destination rectangle, which forces the source image to be resized to fit the rectangle. Other variations include a source rectangle that permits you to specify a portion of the source image to be displayed; and some include both a destination and source rectangle. The following examples capture most of the basic effects that can be achieved. Note that the source image is 192×160 pixels for all examples, and the destination panel is 96×80 pixels.
1.
The source image is drawn at its full size on the target surface. Cropping occurs because the destination panel is smaller than the source image. Graphics g = panel1.CreateGraphics(); g.DrawImage(bmp,0,0);
2.
The source image is scaled to fit the destination rectangle. Rectangle dRect = new Rectangle(new Point(0,0), new Size(panel1.Width,panel1.Height)); g.DrawImage(bmp, dRect); //Or panel1.ClientRectangle
3.
Part of the source rectangle (left corner = 100,0) is selected. Rectangle sRect = new Rectangle(new Point(100,0), new Size(192,160)); g.DrawImage(bmp,0,0,sRect,GraphicsUnit.Pixel);
8.3 Images
4.
Combines examples 2 and 3: A portion of the source rectangle is scaled to fit dimensions of destination rectangle. g.DrawImage(bmp,dRect,sRect, GraphicsUnit.Pixel);
5.
The destination points specify where the upper-left, upper-right, and lower-left point of the original are placed. The fourth point is determined automatically in order to create a parallelogram. Point[]dp = {new Point(10,0),new Point(80,10), new Point(0,70)}; // ul, ur, ll g.DrawImage(bmp,dp);
The DrawImage variations shown here illustrate many familiar image effects: zoom in and zoom out are achieved by defining a destination rectangle larger (zoom in) or smaller (zoom out) than the source image; image skewing and rotation are products of mapping the corners of the original image to three destination points, as shown in the figure to the left of Example 5.
A Note on Displaying Icons Icons (.ico files) do not inherit from the Image class and cannot be rendered by the DrawImage method; instead, they use the Graphics.DrawIcon method. To display one, create an Icon object and pass the file name or Stream object containing the image to the constructor. Then, use DrawIcon to display the image at a desired location. Icon icon = new Icon("c:\\clock.ico"); g.DrawIcon(icon,120,220); // Display at x=120, y=220 icon.Dispose(); // Always dispose of object g.Dispose();
Manipulating Images .NET also supports some more advanced image manipulation techniques that allow an application to rotate, mirror, flip, and change individual pixels in an image. We’ll look at these techniques and also examine the advantage of building an image in memory before displaying it to a physical device.
411
412
Chapter 8
■
.NET Graphics Using GDI+
Rotating and Mirroring Operations that rotate or skew an image normally rely on the DrawImage overload that maps three corners of the original image to destination points that define a parallelogram. void DrawImage(Image image, Point destPoints[])
Recall from an earlier example that the destination points are the new coordinates of the upper-left, upper-right, and lower-left corners of the source image. Figure 8-15 illustrates the effects that can be achieved by altering the destination points. a
b
a
b
c
c
a
b c
Original
Mirrored Figure 8-15
c
a
Flipped
b
Rotated 90˚
Manipulation using DrawImage
The following code is used to create a mirrored image from the original image. Think of the image as a page that has been turned over from left to right: points a and b are switched, and point c is now the lower-right edge. Bitmap bmp = new Bitmap(fname); // Mirror Image Point ptA = new Point(bmp.Width,0); Point ptB = new Point(0,0); Point ptC = new Point(bmp.Width, bmp.Height); Point[]dp = {ptA,ptB,ptC}; g.DrawImage(bmp,dp);
// Get image // Upper left // Upper right // Lower left
Many of these same effects can be achieved using the Bitmap.RotateFlip method, which has this signature: Public void RotateFlip(RotateFlipType rft) RotateFlipType is an enumeration that indicates how many degrees to rotate the image and whether to “flip” it after rotating (available rotations are 90, 180, and 270 degrees). Here are a couple of examples:
8.3 Images
// Rotate 90 degrees bmp.RotateFlip(RotateFlipType.Rotate90FlipNone); // Rotate 90 degrees and flip along the vertical axis bmp.RotateFlip(RotateFlipType.Rotate90FlipY); // Flip horizontally (mirror) bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);
The most important thing to recognize about this method is that it changes the actual image in memory—as opposed to DrawImage, which simply changes it on the drawing surface. For example, if you rotate an image 90 degrees and then rotate it 90 degrees again, the image will be rotated a total of 180 degrees in memory.
Working with a Buffered Image All of the preceding examples are based on drawing directly to a visible panel control on a form. It is also possible to load an image into, or draw your own image onto, an internal Bitmap object before displaying it. This can offer several advantages: • •
•
It permits you to create images such as graphs dynamically and display them in the application or load them into Web pages for display. It improves performance by permitting the application to respond to a Paint event by redrawing a stored image, rather than having to reconstruct the image from scratch. It permits you to keep the current “state” of the image. As long as all transformations, such as rotating or changing colors, are made first to the Bitmap object in memory, it will always represent the current state of the image.
To demonstrate, let’s input a two-color image, place it in memory, change pixels in it, and write it to a panel. Figure 8-16 shows the initial image and the final image after pixels are swapped.
Figure 8-16
Use GetPixel() and SetPixel() to swap pixels
The following code creates a Bitmap object bmpMem that serves as a buffer where the pixels are swapped on the flag before it is displayed. We use the Graphics.From-
413
414
Chapter 8
■
.NET Graphics Using GDI+
Image method to obtain a Graphics object that can write to the image in memory. Other new features to note are the use of GetPixel and SetPixel to read and write
pixels on the image. Graphics g = pan.CreateGraphics(); // Create from a panel Bitmap bmp = new Bitmap("c:\\flag.gif"); g.DrawImage(bmp,0,0); // Draw flag to panel Bitmap bmpMem = new Bitmap(bmp.Width,bmp.Height); Graphics gMem = Graphics.FromImage(bmpMem); gMem.DrawImage(bmp,0,0); // Draw flag to memory // Define a color object for the red pixels Color cnRed = Color.FromArgb(255,214,41,33); // a,r,g,b // Loop through all pixels in image and swap them for (int y=0; y origPoint.X) rectSel.X = origPoint.X; rectSel.Y = origPoint.Y;
8.3 Images
Listing 8-5
Image Viewer: Select Area of Image to Copy (continued)
rectSel.Width = Math.Abs(e.X- origPoint.X)+1; rectSel.Height= Math.Abs(e.Y - origPoint.Y)+1; origPoint = Point.Empty; if (rectSel.Width < 2) selectStatus=false; } private void Mouse_Move(object sender, MouseEventArgs e) { // Tracks mouse movement to draw bounding rectangle if (origPoint != Point.Empty) { Rectangle r; Rectangle rd; // Get rectangle area to invalidate int xop = origPoint.X; if (xop > lastPoint.X) xop= lastPoint.X; int w = Math.Abs(origPoint.X - lastPoint.X)+1; int h = lastPoint.Y - origPoint.Y+1; r = new Rectangle(xop,origPoint.Y,w,h); // Get rectangle area to draw xop = e.X >= origPoint.X ? origPoint.X:e.X; w = Math.Abs(origPoint.X - e.X); h = e.Y - origPoint.Y; rd = new Rectangle(xop, origPoint.Y,w,h); Graphics g = panel1.CreateGraphics(); // Redraw image over previous rectangle g.DrawImage(newBmp,r,r); // Draw rectangle around selected area g.DrawRectangle(Pens.Red,rd); g.Dispose(); lastPoint.X= e.X; lastPoint.Y= e.Y; } }
The logic for creating the rectangles is based on establishing a point of origin where the first MouseDown occurs. The subsequent rectangles then attempt to use that point’s coordinates for the upper left corner. However, if the mouse is moved to the left of the origin, the upper left corner must be based on this x value. This is why the MouseUp and MouseMove routines check to see if the current x coordinate e.x is less than that of the origin.
419
420
Chapter 8
■
.NET Graphics Using GDI+
Copying and Manipulating the Image on the Small Panel The following code is executed when Image – Copy is selected from the menu. The selected area is defined by the rectangle rectSel. The image bounded by this rectangle is drawn to a temporary Bitmap, which is then drawn to panel2. A copy of the contents of panel2 is always maintained in the Bitmap smallBmp. if (selectStatus) { Graphics g = panel2.CreateGraphics(); g.FillRectangle(Brushes.White,panel2.ClientRectangle); Rectangle rd = new Rectangle(0,0,rectSel.Width,rectSel.Height); Bitmap temp = new Bitmap(rectSel.Width,rectSel.Height); Graphics gi = Graphics.FromImage(temp); // Draw selected portion of image onto temp gi.DrawImage(newBmp,rd,rectSel,GraphicsUnit.Pixel); smallBmp = temp; // save image displayed on panel2 // Draw image onto panel2 g.DrawImage(smallBmp,rd); g.Dispose(); resizeLevel = 0; // Keeps track of magnification/reduction }
The plus (+) and minus (–) buttons are used to enlarge or reduce the image on panel2. The actual enlargement or reduction is performed in memory on smallBmp, which holds the original copied image. This is then drawn to the small panel. As
shown in the code here, the magnification algorithm is quite simple: The width and height of the original image are increased in increments of .25 and used as the dimensions of the target rectangle. // Enlarge image Graphics g = panel2.CreateGraphics(); if (smallBmp != null) { resizeLevel= resizeLevel+1; float fac= (float) (1.0+(resizeLevel*.25)); int w = (int)(smallBmp.Width*fac); int h = (int)(smallBmp.Height*fac); Rectangle rd= new Rectangle(0,0,w,h); // Destination rect. Bitmap tempBmp = new Bitmap(w,h); Graphics gi = Graphics.FromImage(tempBmp); // Draw enlarged image to tempBmp Bitmap
8.3 Images
gi.DrawImage(smallBmp,rd); g.DrawImage(tempBmp,rd); // Display enlarged image gi.Dispose(); } g.Dispose();
The code to reduce the image is similar, except that the width and height of the target rectangle are decremented by a factor of .25: resizeLevel= (resizeLevel>-3)?resizeLevel-1:resizeLevel; float fac= (float) (1.0+(resizeLevel*.25)); int w = (int)(smallBmp.Width*fac); int h =(int) (smallBmp.Height*fac);
A Note on GDI and BitBlt for the Microsoft Windows Platform As we have seen in the preceding examples, Graphics.DrawImage is an easy-to-use method for drawing to a visible external device or to a Bitmap object in memory. As a rule, it meets the graphics demands of most programs. However, there are situations where a more flexible or faster method is required. One of the more common graphics requirements is to perform a screen capture of an entire display or a portion of a form used as a drawing area. Unfortunately, GDI+ does not provide a direct way to copy bits from the screen memory. You may also have a graphics-intensive application that requires the constant redrawing of complex images. In both cases, the solution may well be to use GDI—specifically the BitBlt function. If you have worked with the Win32 API, you are undoubtedly familiar with BitBlt. If not, BitBlt, which is short for Bit Block Transfer, is a very fast method for copying bits to and from a screen’s memory, usually with the support of the graphics card. In fact, the DrawImage method uses BitBlt underneath to perform its operations. Even though it is part of the Win32 API, .NET makes it easy to use the BitBlt function. The first step is to use the System.Runtime.InteropServices namespace to provide the DllImportAttribute for the function. This attribute makes the Win32 API available to managed code. [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")] private static extern int BitBlt( IntPtr hDestDC, // Handle to target device context int xDest, // x coordinate of destination int yDest, // y coordinate of destination int nWidth, // Width of memory being copied int nHeight, // Height of memory being copied IntPtr hSrcDC, // Handle to source device context
421
422
Chapter 8
■
.NET Graphics Using GDI+
int xSrc, int ySrc, System.Int32 dwRop
// x coordinate of image source // y coordinate of image source // Copy is specified by 0x00CC0020
);
This function copies a rectangular bitmap from a source to a destination. The source and destination are designated by handles to their device context. (In Windows, a device context is a data structure that describes the object’s drawing surface and where to locate it in memory.) The type of bit transfer performed is determined by the value of the dwRop parameter. A simple copy takes the value shown in the declaration. By changing this value, you can specify that the source and target bits be combined by AND, OR, XOR, and other logical operators. Using bitBlt is straightforward. In this example, the contents of a panel are copied to a Bitmap object in memory. Creating the Graphics object for the panel and Bitmap should be familiar territory. Next, use the Graphics object’s GetHdc method to obtain a handle for the device context for the panel and Bitmap. These are then passed to the bitBlt function along with a ropType argument that tells the function to perform a straight copy operation. // Draw an image on to a panel Graphics g = panel1.CreateGraphics(); g.DrawLine(Pens.Black,10,10,50,50); g.DrawEllipse(Pens.Blue,0,30,40,30); // Create a memory Bitmap object to be the destination Bitmap fxMem = new Bitmap(panel1.Width,panel1.Height); Graphics gfxMem = Graphics.FromImage(fxMem); int ropType= 0x00CC0020; // perform a copy operation // Get references to the device context for the source and target IntPtr HDCSrc= g.GetHdc(); IntPtr HDCMem= gfxMem.GetHdc(); // Copy a rectangular area from the panel of size 100 x 100 bitBlt(HDCMem,0,0,100,100,HDCSrc,0,0,ropType); // Release resources when finished g.ReleaseHdc(HDCSrc); gfxMem.ReleaseHdc(HDCMem); g.Dispose(); gfxMem.Dispose();
Always pair each GetHdc with a ReleaseHdc, and only place calls to GDI functions within their scope. GDI+ operations between the statements are ignored.
8.5 Test Your Understanding
8.4
Summary
GDI+ supports a wide range of graphics-related operations ranging from drawing to image manipulation. All require use of the Graphics object that encapsulates the drawing surface and provides methods for drawing geometric shapes and images to the abstract surface. The graphic is typically rendered to a form or control on the form to be displayed in a user interface. The task of drawing is simplified by the availability of methods to create predefined geometric shapes such as lines, ellipses, and rectangles. These shapes are outlined using a Pen object that can take virtually any color or width, and can be drawn as a solid line or broken into a series of dashes, dots, and combinations of these. The shapes are filled with special brush objects such as SolidBrush, TextureBrush, LinearGradientBrush, and HatchBrush. The Image and Bitmap classes are used in .NET to represent raster-based images. Most of the standard image formats—BMP, GIF, JPG, and PNG—are supported. After an image is loaded or drawn, the Graphics.DrawImage method may be used to rotate, skew, mirror, resize, and crop images as it displays them to the Graphics object’s surface. When custom graphics are included in a form or control, it is necessary to respond to the Paint event, to redraw all or part of the graphics on demand. A program can force this event for a control by invoking the Control.Invalidate method.
8.5
Test Your Understanding
1. What two properties does PaintEventArgs provide a Paint handler routine? Describe the role of each. 2. What method is called to trigger a Paint event for a control? 3. Which image is drawn by the following code? GraphicsPath gp = new GraphicsPath(); gp.AddLine(0,0,60,0); gp.AddLine(0,20,60,20); g.DrawPath(new Pen(Color.Black,2),gp);
423
424
Chapter 8
■
.NET Graphics Using GDI+
4. Which of these statements will cause a compile-time error? a. b.
Brush sb = new SolidBrush(Color.Chartreuse); Brush b = new Brush(Color.Red); c. Brush h = new HatchBrush(HatchStyle.DottedDiamond, Color.Blue,Color.Red);
5. Which of these colors is more transparent? Color a = FromArgb(255,112,128,144); Color b = FromArgb(200,212,128,200);
6. You are drawing an image that is 200×200 pixels onto a panel that is 100×100 pixels. The image is contained in the Bitmap bmp, and the following statement is used: g.DrawImage(bmp, panel1.ClientRectangle);
What percent of the image is displayed? a. b. c.
25% 50% 100%
7. The Russian artist Wassily Kandinsky translated the dance movements of Gret Palucca into a series of schematic diagrams consisting of simple geometric shapes. The following is a computer generated schematic (approximating Kandinsky’s) that corresponds to the accompanying dance position. The schematic is created with a GraphicsPath object and the statements that follow. However, the statements have been rearranged, and your task is to place them in a sequence to draw the schematic. Recall that a GraphicsPath object automatically connects objects.
Graphics g = panel1.CreateGraphics(); g.SmoothingMode = SmoothingMode.AntiAlias; GraphicsPath gp = new GraphicsPath(); gp.AddLine(10,170,30,170); gp.AddLine(40,50,50,20);
8.5 Test Your Understanding
gp.StartFigure(); gp.AddLine(16,100,100,100); gp.AddLine(50,20,145,100); gp.AddLine(100,100,190,180); gp.StartFigure(); gp.AddArc(65,10,120,180,180,80); g.DrawPath(new Pen(Color.Black,2),gp); gp.StartFigure(); gp.AddArc(65,5,120,100,200,70);
8. The following statements are applied to the original image A: Point ptA = new Point(bmp.Height,0); Point ptB = new Point(bmp.Height,bmp.Width); Point ptC = new Point(0,0); Point[]dp = {ptA,ptB,ptC}; g.DrawImage(bmp,dp);
Which image is drawn by the last statement?
425
FONTS, TEXT, AND PRINTING
Topics in This Chapter • Fonts: .NET classes support the creation of individual font objects and font families. Treating a font as an object allows an application to set a font’s style and size through properties. • Text: The Graphics.DrawString method and StringFormat class are used together to draw and format text on a drawing surface. • Printing: The PrintDocument class provides the methods, properties, and events used to control .NET printing. It exposes properties that allow printer selection, page setup, and choice of pages to be printed. • Creating a Custom PrintDocument Class: A custom PrintDocument class provides an application greater control over the printing process and can be used to ensure a consistent format for reports.
9
Chapter 8, “.NET Graphics Using GDI+,” focused on the use of GDI+ for creating graphics and working with images. This chapter continues the exploration of GDI+ with the focus shifting to its use for “drawing text.” One typically thinks of text as being printed—rather than drawn. In the .NET world, however, the display and rendering of text relies on techniques similar to those required to display any graphic: a Graphics object must be created, and its methods used to position and render text strings—the shape of which is determined by the font used. This chapter begins with a look at fonts and an explanation of the relationship between fonts, font families, and typefaces. It then looks at the classes used to create fonts and the options for sizing them and selecting font styles. The Graphics.DrawString method is the primary tool for rendering text and has several overloads that support a number of formatting features: text alignment, text wrapping, the creation of columnar text, and trimming, to name a few. The chapter concludes with a detailed look at printing. An incremental approach is taken. A simple example that illustrates the basic classes is presented. Building on these fundamentals, a more complex and realistic multi-page report with headers, multiple columns, and end-of-page handling is presented. Throughout, the emphasis is on understanding the GDI+ classes that support printer selection, page layout, and page creation.
427
428
Chapter 9
9.1
■
Fonts, Text, and Printing
Fonts
GDI+ supports only OpenType and TrueType fonts. Both of these font types are defined by mathematical representations that allow them to be scaled and rotated easily. This is in contrast to raster fonts that represent characters by a bitmap of a predetermined size. Although prevalent a few years ago, these font types are now only a historical footnote in the .NET world. The term font is often used as a catchall term to describe what are in fact typefaces and font families. In .NET, it is important to have a precise definition for these terms because both Font and FontFamily are .NET classes. Before discussing how to implement these classes, let’s look at the proper definition of these terms. •
•
•
A font family is at the top of the hierarchy and consists of a group of typefaces that may be of any style, but share the same basic appearance. Tahoma is a font family. A typeface is one step up the hierarchy and refers to a set of fonts that share the same family and style but can be any size. Tahoma bold is a typeface. A font defines how a character is represented in terms of font family, style, and size. For example (see Figure 9-1), Tahoma Regular10 describes a font in the Tahoma font family having a regular style—as opposed to bold or italic—and a size of 10 points.
Tahoma normal 8
10
bold
12 . . .
italic
Font Family
bold italic 8 10
12 . . .
Typeface
Font
Figure 9-1 Relationship of fonts, typefaces, and font families
Font Families The FontFamily class has two primary purposes: to create a FontFamily object that is later used to create an instance of a Font, or to provide the names of all font families available on the user’s computer system.
9.1 Fonts
Creating a FontFamily Object Constructors: public FontFamily (string name); public FontFamily (GenericFontFamilies genfamilies);
Parameters: name
A font family name such as Arial or Tahoma.
FontFamily ff = new FontFamily("Arial"); GenericFontFamilies
// Arial family
An enumeration (in the System.Drawing.Text namespace) that has three values: Monospace, Serif, and SansSerif. This constructor is useful when you are interested in a generic font family rather than a specific one. The following constructor specifies a font family in which each character takes an equal amount of space horizontally. (Courier New is a common monospace family.)
FontFamily ff = new FontFamily(GenericFontFamilies.Monospace);
As a rule, the more specific you are about the font family, the more control you have over the appearance of the application, which makes the first constructor preferable in most cases. However, use a generic font family if you are unsure about the availability of a specific font on a user’s computer.
Listing Font Families The FontFamily.Families property returns an array of available font families. This is useful for allowing a program to select a font internally to use for displaying fonts to users, so they can select one. The following code enumerates the array of font families and displays the name of the family using the Name property; it also uses the IsStyleAvailable method to determine if the family supports a bold style font. string txt = ""; foreach (FontFamily ff in FontFamily.Families) { txt += ff.Name; if (ff.IsStyleAvailable(FontStyle.Bold))txt= += " (B)"; txt += "\r\n"; // Line feed } textBox1.Text = txt; // Place font family names in textbox
429
430
Chapter 9
■
Fonts, Text, and Printing
The Font Class Although several constructors are available for creating fonts, they fall into two categories based on whether their first parameter is a FontFamily type or a string containing the name of a FontFamily. Other parameters specify the size of the font, its style, and the units to be used for sizing. Here are the most commonly used constructors: Constructors: public public public public
Font(FontFamily ff, Float Font(FontFamily ff, Float Font(FontFamily ff, Float Font(FontFamily ff, Float GraphicsUnit unit);
emSize); emSize, FontStyle style); emSize, GraphicsUnit unit); emSize, FontStyle style,
public public public public
Font(String famname, Float Font(String famname, Float Font(String famname, Float Font(String famname, Float GraphicsUnit unit);
emSize); emSize, FontStyle style); emSize, GraphicsUnit unit); emSize, FontStyle style,
Parameters: emSize
The size of the font in terms of the GraphicsUnit. The default GraphicsUnit is Point.
style
A FontStyle enumeration value: Bold, Italic, Regular, Strikeout, or Underline. All font families do not support all styles.
unit
The units in which the font is measured. This is one of the GraphicsUnit enumeration values: Point—1/72nd inch Display—1/96th inch Document—1/300th inch
Inch Millimeter Pixel (based on device resolution)
GraphicsUnits and sizing are discussed later in this section.
Creating a Font Creating fonts is easy; the difficulty lies in deciding which of the many constructors to use. If you have already created a font family, the simplest constructor is FontFamily ff = new FontFamily("Arial"); Font normFont = new Font(ff,10); // Arial Regular 10 pt.
9.1 Fonts
The simplest approach, without using a font family, is to pass the typeface name and size to a constructor: Font normFont = new Font("Arial",10)
// Arial Regular 10 point
By default, a Regular font style is provided. To override this, pass a FontStyle enumeration value to the constructor: Font boldFont = new Font("Arial",10,FontStyle.Bold); Font bldItFont = new Font("Arial",10, FontStyle.Bold | FontStyle.Italic); // Arial bold italic 10
The second example illustrates how to combine styles. Note that if a style is specified that is not supported by the font family, an exception will occur. The Font class implements the IDisposable interface, which means that a font’s Dispose method should be called when the font is no longer needed. As we did in the previous chapter with the Graphics object, we can create the Font object inside a using construct, to ensure the font resources are freed even if an exception occurs. using (Font normFont = new Font("Arial",12)) {
Using Font Metrics to Determine Font Height The term font metrics refers to the characteristics that define the height of a font family. .NET provides both Font and FontClass methods to expose these values. To best understand them, it is useful to look at the legacy of typesetting and the terms that have been carried forward from the days of Gutenberg’s printing press to the .NET Framework Class Library. In typography, the square grid (imagine graph paper) used to lay out the outline (glyph) of a font is referred to as the em square. It consists of thousands of cells— each measured as one design unit. The outline of each character is created in an em square and consists of three parts: a descent, which is the part of the character below the established baseline for all characters; the ascent, which is the part above the baseline; and the leading, which provides the vertical spacing between characters on adjacent lines (see Figure 9-2).
Ascender Line
Key
Baseline Descender Line Figure 9-2
Leading Ascent Descent
Font metrics: the components of a font
Font Height
431
432
Chapter 9
■
Fonts, Text, and Printing
Table 9-1 lists the methods and properties used to retrieve the metrics associated with the em height, descent, ascent, and total line space for a font family. Most of the values are returned as design units, which are quite easy to convert to a GraphicsUnit. The key is to remember that the total number of design units in the em is equivalent to the base size argument passed to the Font constructor. Here is an example of retrieving metrics for an Arial 20-point font: FontFamily ff = new FontFamily("Arial"); Font myFont = new Font(ff,20); // Height is 20 points int emHeight = ff.GetEmHeight(FontStyle.Regular); // int ascHeight = ff.GetCellAscent(FontStyle.Regular); // int desHeight = ff.GetCellDescent(FontStyle.Regular); // int lineSpace = ff.GetLineSpacing(FontStyle.Regular); // // Get Line Height in Points (20 x (2355/2048)) float guHeight = myFont.Size * (lineSpace / emHeight); // float guHeight2 = myFont.GetHeight(); // 30.66 pixels
2048 1854 434 2355 22.99
The primary value of this exercise is to establish familiarity with the terms and units used to express font metrics. Most applications that print or display lines of text are interested primarily in the height of a line. This value is returned by the Font.GetHeight method and is also available through the Graphics.MeasureString method described in the next section. Table 9-1 Using Font and FontFamily to Obtain Font Metrics Member
Units
Description
FontFamily.GetEmHeight
Design units
The height of the em square used to design the font family. TrueType fonts usually have a value of 2,048.
FontFamily.GetCellAscent
Design units
The height of a character above the base line.
FontFamily.GetCellDescent
Design units
The height of a character below the base line.
FontFamily.GetLineSpacing
Design units
The total height reserved for a character plus line spacing. The sum of CellAscent, CellDescent, and Leading (see Figure 9-2). This value is usually 12 to 15 percent greater than the em height.
Font.Size
Graphics unit
The base size (size passed to constructor).
9.2 Drawing Text Strings
Table 9-1 Using Font and FontFamily to Obtain Font Metrics (continued) Member
Units
Description
Font.SizeInPoints
Points
The base size in points.
Font.GetHeight
Pixels
The total height of a line. Calculated by converting LineSpacing value to pixels.
Definitions: cell height
= ascent + descent
em height
= cell height – internal leading
line spacing = cell height + external leading
9.2
Drawing Text Strings
The Graphics.DrawString method is the most straightforward way to place text on a drawing surface. All of its overloaded forms take a string to be printed, a font to represent the text, and a brush object to paint the text. The location where the text is to be printed is specified by a Point object, x and y coordinates, or a Rectangle object. The most interesting parameter is an optional StringFormat object that provides the formatting attributes for the DrawString method. We’ll examine it in detail in the discussion on formatting. Here are the overloads for DrawString. Note that StringFormat is optional in each. Overloads: public DrawString(string, font, brush, PointF [,StringFormat]); public DrawString(string, font, brush, float, float [,StringFormat]); public DrawString(string, font, brush, RectangleF [,StringFormat]);
Example: Font regFont = new Font("Tahoma",12); String s = "ice mast high came floating by as green as emerald."; // Draw text beginning at coordinates (20,5) g.DrawString(s, regFont, Brushes.Black, 20,5); regFont.Dispose();
433
434
Chapter 9
■
Fonts, Text, and Printing
In this example, the upper-left corner of the text string is located at the x,y coordinate 20 pixels from the left edge and 5 pixels from the top of the drawing surface. If the printed text extends beyond the boundary of the drawing surface, the text is truncated. You may want this in some cases, but more often you’ll prefer that long lines be broken and printed as multiple lines.
Drawing Multi-Line Text Several Drawstring overloads receive a rectangle to define where the output string is drawn. Text drawn into these rectangular areas is automatically wrapped to the next line if it reaches beyond the rectangle’s boundary. The following code displays the fragment of poetry in an area 200 pixels wide and 50 pixels high. String s = "and ice mast high came floating by as green as emerald." // All units in pixels RectangleF rf = new RectangleF(20,5,200,50); // Fit text in rectangle g.Drawstring(s,regFont,Brushes.Black, rf);
Word wrapping is often preferable to line truncation, but raises the problem of determining how many lines of text must be accommodated. If there are more lines of text than can fit into the rectangle, they are truncated. To avoid truncation, you could calculate the height of the required rectangle by taking into account the font (f), total string length(s), and rectangle width (w). It turns out that .NET Graphics.MeasureString method performs this exact operation. One of its overloads takes the string, font, and desired line width as arguments, and returns a SizeF object whose Width and Height properties provide pixel values that define the required rectangle. SizeF sf = g.MeasureString(String s, Font f, int w);
Using this method, the preceding code can be rewritten to handle the dynamic creation of the bounding rectangle: Font regFont = new Font("Tahoma",12); String s = "and ice mast high came floating by as green as emerald." int lineWidth = 200; SizeF sf = g.MeasureString(s, regFont, lineWidth); // Create rectangular drawing area based on return // height and width RectangleF rf = new RectangleF(20,5,sf.Width, sf.Height); // Draw text in rectangle
9.2 Drawing Text Strings
g.Drawstring(s,regFont,Brushes.Black, rf); // Draw rectangle around text g.DrawRectangle(Pens.Red,20F,5F,rf.Width, rf.Height);
Note that DrawString recognizes newline (\r\n) characters and creates a line break when one is encountered.
Formatting Strings with the StringFormat Class When passed as an argument to the DrawString method, a StringFormat object can provide a number of formatting effects. It can be used to define tab positions, set column widths, apply right or left justification, and control text wrapping and truncation. As we will see in the next section, it is the primary tool for creating formatted reports. The members that we will make heaviest use of are shown in Table 9-2. Table 9-2 Important StringFormat Members Member
Description
Alignment
A StringAlignment enumeration value: StringAlignment.Center—Text is centered in layout rectangle. StringAlignment.Far—Text is aligned to the right for left-to-right text. StringAlignment.Near—Text is aligned to the left for left-to-right text.
Trimming
A StringTrimming enumeration value that specifies how to trim characters that do not completely fit in the layout rectangle: StringTrimming.Character—Text is trimmed to the nearest character. StringTrimming.EllipsisCharacter—Text is trimmed to the nearest
character and an ellipsis (...) is placed at the end of the line. StringTrimming.Word—Text is trimmed to the nearest word. SetTabStops
Takes two parameters: SetTabStops(firstTabOffset, tabStops) FirstTabOffset—Number of spaces between beginning of line and
first tab stop. TabStops—Array of distances between tab stops. FormatFlags
This bit-coded property provides a variety of options for controlling print layout when printing within a rectangle. StringFormatFlags.DirectionVertical—Draws text from
top-to-bottom. StringFormatFlags.LineLimit—Only entire lines are displayed
within the rectangle. StringFormatFlags.NoWrap—Disables text wrapping. The result is
that text is printed on one line only, irrespective of the rectangle’s height.
435
436
Chapter 9
■
Fonts, Text, and Printing
Using Tab Stops Tab stops provide a way to align proportionate-spaced font characters into columns. To set up tab stops, you create a StringFormat object, use its SetTabStops method to define an array of tab positions, and then pass this object to the DrawString method along with the text string containing tab characters (\t).
Core Note If no tab stops are specified, default tab stops are set up at intervals equal to four times the size of the font. A 10-point font would have default tabs every 40 points.
As shown in Table 9-2, the SetTabStops method takes two arguments: the offset from the beginning of the line and an array of floating point values that specify the distance between tab stops. Here is an example that demonstrates various ways to define tab stops: float[] tStops = {50f, 100f, 100f}; //Stops at: 50, 150, and 250 float[] tStops = {50f}; // Stops at: 50, 100, 150
You can see that it is not necessary to specify a tab stop for every tab. If a string contains a tab for which there is no corresponding tab stop, the last tab stop in the array is repeated. Listing 9-1 demonstrates using tabs to set column headers.
Listing 9-1
Using Tab Stops to Display Columns of Data
private void RePaint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; Font hdrFont = new Font("Arial", 10,FontStyle.Bold); Font bdyFont = new Font("Arial", 10); // (1) Create StringFormat Object StringFormat strFmt = new StringFormat(); // (2) Define Tab stops float[] ts = {140,60,40}; strFmt.SetTabStops(0, ts); // (3) Define column header text to be printed with tabs string header = "Artist\tCountry\tBorn\tDied";
9.2 Drawing Text Strings
Listing 9-1
Using Tab Stops to Display Columns of Data (continued)
// (4) Print column headers g.DrawString(header, hdrFont, Brushes.Black,10,10,strFmt); // Print one line below header string artist = "Edouard Manet\tEngland\t1832\t1892"; g.DrawString(artist,bdyFont,Brushes.Black,10, 10 + bdyFont.GetHeight(), strFmt); bdyFont.Dispose(); hdrFont.Dispose(); }
Figure 9-3 shows the four-column output from this code. Note that the second column begins at the x coordinate 150, which is the first tab stop (140) plus the x coordinate (10) specified in DrawString.
Figure 9-3
Printing with tab stops
The unit of measurement in this example is a pixel. This unit of measurement is determined by the Graphics.PageUnit property. To override the default (pixels), set the property to a GraphicsUnit enumeration value—for example, g.PageUnit = GraphicsUnit.Inch. Be aware that all subsequent drawing done with the Graphics object will use these units.
Core Note The use of tab spaces only supports left justification for proportionate fonts. If you need right justification—a virtual necessity for displaying financial data—pass a rectangle that has the appropriate coordinates to the DrawString method. Then, set the Alignment property of StringFormat to StringAlignment.Far.
437
438
Chapter 9
■
Fonts, Text, and Printing
String Trimming, Alignment, and Wrapping The StringFormat Trimming and Alignment properties dictate how text is placed within a RectangleF object. Alignment works as you would expect, allowing you to center, right justify, or left justify a string. Trimming specifies how to truncate a string that extends beyond the boundaries of a rectangle when wrapping is not in effect. The basic options are to truncate on a word or character. The following code segments demonstrate some of the common ways these properties can be used to format text strings.
Example 1: Printing Without a StringFormat Object Font fnt = new Font("Tahoma",10,FontStyle.Bold); RectangleF r = new RectangleF(5,5,220,60); string txt = "dew drops are the gems of morning"; g.DrawString(txt,fnt,Brushes.Black,r); g.DrawRectangle(Pens.Red,r.X,r.Y,r.Width,r.Height);
dew drops are the gems of morning
Example 2: Printing with NoWrap Option StringFormat strFmt = new StringFormat(); strFmt.FormatFlags = StringFormatFlags.NoWrap; g.DrawString(txt,fnt,Brushes.Black,r,strFmt);
dew drops are the gems of mor
Example 3: Printing with NoWrap and Clipping on a Word StringFormat strFmt = new StringFormat(); strFmt.FormatFlags = StringFormatFlags.NoWrap; strFmt.Trimming = StringTrimming.Word; g.DrawString(txt,fnt,Brushes.Black,r,strFmt);
dew drops are the gems of
9.3 Printing
Example 4: Printing with NoWrap, Clipping on Word, and Right Justification StringFormat strFmt = new StringFormat(); strFmt.FormatFlags = StringFormatFlags.NoWrap; strFmt.Trimming = StringTrimming.Word; strFmt.Alignment = StringAlignment.Far; g.DrawString(txt,fnt,Brushes.Black,r,strFmt);
dew drops are the gems of morning
StringFormat also has a LineAlignment property that permits a text string to be centered vertically within a rectangle. To demonstrate, let’s add two statements to Example 4: strFmt.Alignment = StringAlignment.Center; strFmt.LineAlignment = StringAlignment.Center;
dew drops are the gems of morning
9.3
Printing
The techniques discussed in Sections 9.1 and 9.2 are device independent, which means they can be used for drawing to a printer as well as a screen. This section deals specifically with the task of creating reports intended for output to a printer. For complex reporting needs, you may well turn to Crystal Reports—which has special support in Visual Studio.NET—or SQL Server Reports. However, standard reports can be handled quite nicely using the native .NET classes available. Moreover, this approach enables you to understand the fundamentals of .NET printing and apply your knowledge of event handling and inheritance to customize the printing process.
Overview The PrintDocument class—a member of the System.Drawing.Printing namespace—provides the methods, properties, and events that control the print process. Consequently, the first step in setting up a program that sends output to a printer is to create a PrintDocument object.
439
440
Chapter 9
■
Fonts, Text, and Printing
PrintDocument pd = new PrintDocument();
The printing process in initiated when the PrintDocument.Print method is invoked. As shown in Figure 9-4, this triggers the BeginPrint and PrintPage events of the PrintDocument class.
Y
PrintDocument .Print
BeginPrint event
PrintPage event
HasMore Pages
N
EndPrint event
Figure 9-4 PrintDocument events that occur during the printing process
An event handler wired to the PrintPage event contains the logic and statements that produce the output. This routine typically determines how many lines can be printed—based on the page size—and contains the DrawString statements that generate the output. It is also responsible for notifying the underlying print controller whether there are more pages to print. It does this by setting the HasMorePages property, which is passed as an argument to the event handler, to true or false. The basic PrintDocument events can be integrated with print dialog boxes that enable a user to preview output, select a printer, and specify page options. Listing 9-2 displays a simple model for printing that incorporates the essential elements required. Printing is initiated when btnPrint is clicked.
Listing 9-2
Basic Steps in Printing
using System.Drawing; using System.Drawing.Printing; using System.Windows.Forms; // Code for Form class goes here // Respond to button click private void btnPrint_Click(object sender, System.EventArgs e) { PrintReport(); } // Set up overhead for printing private void PrintReport() {
9.3 Printing
Listing 9-2
Basic Steps in Printing (continued)
// (1) Create PrintDocument object PrintDocument pd = new PrintDocument(); // (2) Create PrintDialog PrintDialog pDialog = new PrintDialog(); pDialog.Document = pd; // (3) Create PrintPreviewDialog PrintPreviewDialog prevDialog = new PrintPreviewDialog(); prevDialog.Document = pd; // (4) Tie event handler to PrintPage event pd.PrintPage += new PrintPageEventHandler(Inven_Report); // (5) Display Print Dialog and print if OK received if (pDialog.ShowDialog()== DialogResult.OK) { pd.Print(); // Invoke PrintPage event } } private void Inven_Report(object sender, PrintPageEventArgs e) { Graphics g = e.Graphics; Font myFont = new Font("Arial",10); g.DrawString("Sample Output",myFont,Brushes.Black,10,10); myFont.Dispose(); } }
This simple example illustrates the rudiments of printing, but does not address issues such as handling multiple pages or fitting multiple lines within the boundaries of a page. To extend the code to handle these real-world issues, we need to take a closer look at the PrintDocument class.
PrintDocument Class Figure 9-5 should make it clear that the PrintDocument object is involved in just about all aspects of printing: It provides access to paper size, orientation, and document margins through the DefaultPageSettings class; PrinterSettings allows selection of a printer as well as the number of copies and range of pages to be printed; and event handlers associated with the PrintDocument events take care of initialization and cleanup chores related to the printing process. The PrintController class works at a level closer to the printer. It is used behind the scenes to control the print preview process and tell the printer exactly how to print a document.
441
442
Chapter 9
■
Fonts, Text, and Printing
PrintDocument PageSettings DefaultPageSettings RectangleF Bounds—Size of page bool Landscape—Gets or sets orientation Margins Margins—Gets or sets left, right, top, and bottom page margins PaperSize PaperSize—Width and height of paper in 1/100th inches PrinterSettings PrinterSettings int Copies—Number of copies to print int FromPage—First page to print int ToPage—Last page to print stringCollection InstalledPrinters—Printers installed on the computer string PrinterName—Name of target printer PrintController PrintController StandardPrintController PrintControllerwithStatusDialog PreviewPrintController events BeginPrint—Occurs when Print is called, before first page is printed EndPrint—Occurs after last page is printed PrintPage—Occurs when page needs to be printed QueryPageSettingsEvent—Occurs before each PrintPage event Figure 9-5 Selected PrintDocument properties and events
An understanding of these classes and events is essential to implementing a robust and flexible printing application that provides users full access to printing options.
Printer Settings The PrinterSettings object maintains properties that specify the printer to be used and how the document is to be printed—page range, number of copies, and whether collating or duplexing is used. These values can be set programmatically or by allowing the user to select them from the Windows PrintDialog component. Note that when a user selects an option on the PrintDialog dialog box, he is actually setting a property on the underlying PrinterSettings object.
9.3 Printing
Selecting a Printer The simplest approach is to display the PrintDialog window that contains a drop-down list of printers: PrintDocument pd = new PrintDocument(); PrintDialog pDialog = new PrintDialog(); pDialog.Document = pd; if (pDialog.ShowDialog()== DialogResult.OK) { pd.Print(); // Invoke PrintPage event }
You can also create your own printer selection list by enumerating the InstalledPrinters collection: // Place names of printer in printerList ListBox foreach(string pName in PrinterSettings.InstalledPrinters) printerList.Items.Add(pName);
After the printer is selected, it is assigned to the PrinterName property: string printer= printerList.Items[printerList.SelectedIndex].ToString(); pd.PrinterSettings.PrinterName = printer;
Selecting Pages to Print The PrinterSettings.PrintRange property indicates the range of pages to be printed. Its value is a PrintRange enumeration value—AllPages, Selection, or SomePages—that corresponds to the All, Pages, and Selection print range options on the PrintDialog form. If Pages is selected, the PrinterSettings.FromPage and ToPage properties specify the first and last page to print. There are several things to take into consideration when working with a print range: •
•
•
To make the Selection and Pages radio buttons available on the PrintDialog form, set PrintDialog.AllowSomePages and PrintDialog.AllowSelection to true. The program must set the FromPage and ToPage values before displaying the Print dialog box. In addition, it’s a good practice to set the MinimumPage and MaximumPage values to ensure the user does not enter an invalid page number. Keep in mind that the values entered on a PrintDialog form do nothing more than provide parameters that are available to the application. It is the responsibility of the PrintPage event handler to implement the logic that ensures the selected pages are printed.
443
444
Chapter 9
■
Fonts, Text, and Printing
The following segment includes logic to recognize a page range selection: pDialog.AllowSomePages = true; pd.PrinterSettings.FromPage =1; pd.PrinterSettings.ToPage = maxPg; pd.PrinterSettings.MinimumPage=1; pd.PrinterSettings.MaximumPage= maxPg; if (pDialog.ShowDialog()== DialogResult.OK) { maxPg = 5; // Last page to print currPg= 1; // Current page to print if (pDialog.PrinterSettings.PrintRange == PrintRange.SomePages) { currPg = pd.PrinterSettings.FromPage; maxPg = pd.PrinterSettings.ToPage; } pd.Print(); // Invoke PrintPage event }
This code assigns the first and last page to be printed to currPg and maxPg. These both have class-wide scope and are used by the PrintPage event handler to determine which pages to print.
Setting Printer Resolution The PrinterSettings class exposes all of the print resolutions available to the printer through its PrinterResolutions collection. You can loop through this collection and list or select a resolution by examining the Kind property of the contained PrinterResolution objects. This property takes one of five PrinterResolutionKind enumeration values: Custom, Draft, High, Low, or Medium. The following code searches the PrinterResolutions collection for a High resolution and assigns that as a PrinterSettings value: foreach (PrinterResolution pr in pd.PrinterSettings.PrinterResolutions) { if (pr.Kind == PrinterResolutionKind.High) { pd.PageSettings.PrinterResolution = pr; break; } }
9.3 Printing
Page Settings The properties of the PageSettings class define the layout and orientation of the page being printed to. Just as the PrinterSettings properties correspond to the PrintDialog, the PageSettings properties reflect the values of the PageSetupDialog. PageSetupDialog ps = new PageSetupDialog(); ps.Document = pd; // Assign reference to PrinterDocument ps.ShowDialog();
This dialog box lets the user set all the margins, choose landscape or portrait orientation, select a paper type, and set printer resolution. These values are exposed through the DefaultPageSettings properties listed in Figure 9-5. As we will see, they are also made available to the PrintPage event handler through the PrintPageEventArgs parameter and to the QueryPageSettingsEvent through its QueryPageSettingsEventArgs parameter. The latter can update the values, whereas PrintPage has read-only access.
850
1100
100
MarginBounds: (100,100,650,900) Figure 9-6
Page settings layout
Figure 9-6 illustrates the layout of a page that has the following DefaultPageSettings values: Bounds.X = 0; Bounds.Y = 0; Bounds.Width = 850;
445
446
Chapter 9
■
Fonts, Text, and Printing
Bounds.Height = 1100; PaperSize.PaperName = "Letter"; PaperSize.Height = 1100; PaperSize.Width = 850; Margins.Left = 100; Margins.Right = 100; Margins.Top = 100; Margins.Bottom = 100;
All measurements are in hundredths of an inch. The MarginBounds rectangle shown in the figure represents the area inside the margins. It is not a PrinterSettings property and is made available only to the PrintPage event handler.
Core Note Many printers preserve an edge around a form where printing cannot occur. On many laser printers, for example, this is one-quarter of an inch. In practical terms, this means that all horizontal coordinates used for printing are shifted; thus, if DrawString is passed an x coordinate of 100, it actually prints at 125. It is particularly important to be aware of this when printing on preprinted forms where measurements must be exact.
PrintDocument Events Four PrintDocument events are triggered during the printing process: BeginPrint, QueryPageSettingsEvent, PrintPage, and EndPrint. As we’ve already seen, PrintPage is the most important of these from the standpoint of code development because it contains the logic and statements used to generate the printed output. It is not necessary to handle the other three events, but they do provide a handy way to deal with the overhead of initialization and disposing of resources when the printing is complete.
BeginPrint Event This event occurs when the PrintDocument.Print method is called and is a useful place to create font objects and open data connections. The PrintEventHandler delegate is used to register the event handler routine for the event: pd.BeginPrint += new PrintEventHandler(Rpt_BeginPrint);
9.3 Printing
This simple event handler creates the font to be used in the report. The font must be declared to have scope throughout the class. private void Rpt_BeginPrint(object sender, PrintEventArgs e) { rptFont = new Font("Arial",10); lineHeight= (int)rptFont.GetHeight(); // Line height }
EndPrint Event This event occurs after all printing is completed and can be used to destroy resources no longer needed. Associate an event handler with the event using pd.EndPrint += new PrintEventHandler(Rpt_EndPrint);
This simple event handler disposes of the font created in the BeginPrint handler: private void Rpt_EndPrint(object sender, PrintEventArgs e) { rptFont.Dispose(); }
QueryPageSettingsEvent Event This event occurs before each page is printed and provides an opportunity to adjust the page settings on a page-by-page basis. Its event handler is associated with the event using the following code: pd.QueryPageSettings += new QueryPageSettingsEventHandler(Rpt_Query);
The second argument to this event handler exposes a PageSettings object and a Cancel property that can be set to true to cancel printing. This is the last opportunity before printing to set any PageSettings properties, because they are read-only in the PrintPage event. This code sets special margins for the first page of the report: private void Rpt_Query(object sender, QueryPageSettingsEventArgs e) { // This is the last chance to change page settings // If first page, change margins for title if (currPg ==1) e.PageSettings.Margins = new Margins(200,200,200,200); else e.PageSettings.Margins = new Margins(100,100,100,100); }
447
448
Chapter 9
■
Fonts, Text, and Printing
This event handler should be implemented only if there is a need to change page settings for specific pages in a report. Otherwise, the DefaultPageSettings properties will prevail throughout.
PrintPage Event The steps required to create and print a report fall into two categories: setting up the print environment and actually printing the report. The PrinterSettings and PageSettings classes that have been discussed are central to defining how the report will look. After their values are set, it’s the responsibility of the PrintPage event handler to print the report to the selected printer, while being cognizant of the paper type, margins, and page orientation. Figure 9-7 lists some of the generic tasks that an event handler must deal with in generating a report. Although the specific implementation of each task varies by application, it’s a useful outline to follow in designing the event handler code. We will see an example shortly that uses this outline to implement a simple report application.
Select a Printer Select Paper Size Prepare Data Preview Output PrintDocument.Print
Define Printing Environment Figure 9-7
PrintPage Event
Headers and Page Numbering Print Selected Pages Handle Page Breaks Text Formatting and Line Spacing
PrintPage Event Handler Tasks required to print a report
Defining the PrintPage Event Handler The event handler method matches the signature of the PrintPageEventHandler delegate (refer to Listing 9-2): public delegate void PrintPageEventHandler( object sender, PrintPageEventArgs e);
9.3 Printing
The PrintPageEventArgs argument provides the system data necessary for printing. As shown in Table 9-3, its properties include the Graphics object, PageSettings object, and a MarginBounds rectangle—mentioned earlier—that defines the area within the margins. These properties, along with variables defined at a class level, provide all the information used for printing. Table 9-3 PrintPageEventArgs Members Property
Description
Cancel
Boolean value that can be set to true to cancel the printing.
Graphics
The Graphics object used to write to printer.
HasMorePages
Boolean value indicating whether more pages are to be printed. Default is false.
MarginBounds
Rectangular area representing the area within the margins.
PageBounds
Rectangular area representing the entire page.
PageSettings
Page settings for the page to be printed.
Previewing a Printed Report The capability to preview a report onscreen prior to printing—or as a substitute for printing—is a powerful feature. It is particularly useful during the development process where a screen preview can reduce debugging time, as well as your investment in print cartridges. To preview the printer output, you must set up a PrintPreviewDialog object and set its Document property to an instance of the PrintDocument: PrintPreviewDialog prevDialog = new PrintPreviewDialog(); prevDialog.Document = pd;
The preview process is invoked by calling the ShowDialog method: prevDialog.ShowDialog();
After this method is called, the same steps are followed as in actually printing the document. The difference is that the output is displayed in a special preview window (see Figure 9-8). This provides the obvious advantage of using the same code for both previewing and printing.
449
450
Chapter 9
■
Fonts, Text, and Printing
Figure 9-8 Report can be previewed before printing
A Report Example This example is intended to illustrate the basic elements of printing a multi-page report. It includes a data source that provides an unknown number of records, a title and column header for each page, and detailed rows of data consisting of left-justified text and right-justified numeric data.
Data Source for the Report The data in a report can come from a variety of sources, although it most often comes from a database. Because database access is not discussed until Chapter 11, “ADO.NET,” let’s use a text file containing comma-delimited inventory records as the data source. Each record consists of a product ID, vendor ID, description, and price: 1000761,1050,2PC/DRESSER/MIRROR,185.50
A StreamReader object is used to load the data and is declared to have class-wide scope so it is available to the PrintPage event handler: // Using System.IO namespace is required // StreamReader sr; is set up in class declaration sr = new StreamReader("c:\\inventory.txt");
The PrintPage event handler uses the StreamReader to input each inventory record from the text file as a string. The string is split into separate fields that are stored in the prtLine array. The event handler also contains logic to recognize page breaks and perform any column totaling desired.
9.3 Printing
Code for the Application Listing 9-3 contains the code for the PrintDocument event handlers implemented in the application. Because you cannot pass your own arguments to an event handler, the code must rely on variables declared at the class level to maintain state information. These include the following: StreamReader sr; // StreamReader to read inventor from file string[]prtLine; // Array containing fields for one record Font rptFont; // Font for report body Font hdrFont; // Font for header string title= "Acme Furniture: Inventory Report"; float lineHeight; // Height of a line (100ths inches)
The fonts and StreamReader are initialized in the BeginPrint event handler. The corresponding EndPrint event handler then disposes of the two fonts and closes the StreamReader.
Listing 9-3
Creating a Report
// pd.PrintPage += new PrintPageEventHandler(Inven_Report); // pd.BeginPrint += new PrintEventHandler(Rpt_BeginPrint); // pd.EndPrint += new PrintEventHandler(Rpt_EndPrint); // // BeginPrint event handler private void Rpt_BeginPrint(object sender, PrintEventArgs e) { // Create fonts to be used and get line spacing. rptFont = new Font("Arial",10); hdrFont = new Font(rptFont,FontStyle.Bold); // insert code here to set up Data Source... } // EndPrint event Handler private void Rpt_EndPrint(object sender, PrintEventArgs e) { // Remove Font resources rptFont.Dispose(); hdrFont.Dispose(); sr.Close(); // Close StreamReader } // PrintPage event Handler private void Inven_Report(object sender, PrintPageEventArgs e) {
451
452
Chapter 9
■
Listing 9-3
Fonts, Text, and Printing
Creating a Report (continued)
Graphics g = e.Graphics; int xpos= e.MarginBounds.Left; int lineCt = 0; // Next line returns 15.97 for Arial-10 lineHeight = hdrFont.GetHeight(g); // Calculate maximum number of lines per page int linesPerPage = int((e.MarginBounds.Bottom – e.MarginBounds.Top)/lineHeight –2); float yPos = 2* lineHeight+ e.MarginBounds.Top; int hdrPos = e.MarginBounds.Top; // Call method to print title and column headers PrintHdr(g,hdrPos, e.MarginBounds.Left); string prod; char[]delim= {','}; while(( prod =sr.ReadLine())!=null) { prtLine= prod.Split(delim,4); yPos += lineHeight; // Get y coordinate of line PrintDetail(g,yPos); // Print inventory record if(lineCt > linesPerPage) { e.HasMorePages= true; break; } } } private void PrintHdr( Graphics g, int yPos, int xPos) { // Draw Report Title g.DrawString(title,hdrFont,Brushes.Black,xPos,yPos); // Draw Column Header float[] ts = {80, 80,200}; StringFormat strFmt = new StringFormat(); strFmt.SetTabStops(0,ts); g.DrawString("Code\tVendor\tDescription\tCost", hdrFont,Brushes.Black,xPos,yPos+2*lineHeight,strFmt); } private void PrintDetail(Graphics g, float yPos) { int xPos = 100; StringFormat strFmt = new StringFormat(); strFmt.Trimming = StringTrimming.EllipsisCharacter; strFmt.FormatFlags = StringFormatFlags.NoWrap; RectangleF r = new RectangleF(xPos+160,yPos, 200,lineHeight);
9.3 Printing
Listing 9-3
Creating a Report (continued)
// Get data fields from array string invenid = prtLine[0]; string vendor = prtLine[1]; string desc = prtLine[2]; decimal price = decimal.Parse(prtLine[3]); g.DrawString(invenid, rptFont,Brushes.Black,xPos, yPos); g.DrawString(vendor, rptFont,Brushes.Black,xPos+80, yPos); // Print description within a rectangle g.DrawString(desc, rptFont,Brushes.Black,r,strFmt); // Print cost right justified strFmt.Alignment = StringAlignment.Far; // Right justify strFmt.Trimming= StringTrimming.None; g.DrawString(price.ToString("#,###.00"), rptFont,Brushes.Black, xPos+400,yPos,strFmt); }
The PrintPage event handler Inven_Report directs the printing process by calling PrintHdr to print the title and column header on each page and PrintDetail to print each line of inventory data. Its responsibilities include the following: • •
•
Using the MarginBounds rectangle to set the x and y coordinates of the title at the upper-left corner of the page within the margins. Calculating the maximum number of lines to be printed on a page. This is derived by dividing the distance between the top and bottom margin by the height of a line. It then subtracts 2 from this to take the header into account. Setting the HasMorePages property to indicate whether more pages remain to be printed.
The PrintHdr routine is straightforward. It prints the title at the coordinates passed to it, and then uses tabs to print the column headers. The PrintDetail method is a bit more interesting, as it demonstrates some of the classes discussed earlier in the chapter. It prints the inventory description in a rectangle and uses the StringFormat class to prevent wrapping and specify truncation on a character. StringFormat is also used to right justify the price of an item in the last column. Figure 9-9 shows an example of output from this application. Measured from the left margin, the first three columns have an x coordinate of 0, 80, and 160, respectively. Note that the fourth column is right justified, which means that its x coordinate of 400 specifies where the right edge of the string is positioned. Vertical spacing is determined by the lineHeight variable that is calculated as float lineHeight = hdrFont.GetHeight(g);
453
454
Chapter 9
■
Fonts, Text, and Printing
This form of the GetHeight method returns a value based on the GraphicsUnit of the Graphics object passed to it. By default, the Graphics object passed to the BeginPrint event handler has a GraphicsUnit of 100 dpi. The margin values and all coordinates in the example are in hundredths of an inch. .NET takes care of automatically scaling these units to match the printer’s resolution.
Acme Furniture: Inventory Report Code 1000758 1000761 1000762 1000764 1000768
Vendor 1050 1050 1050 1050 1050 Figure 9-9
Description 2PC/DRESSER/MIRROR 2PC/DRESSER/MIRROR FRAME MIRROR HUTCH MIRROR DESK-NMFC 81200
Cost 120.50 185.50 39.00 84.00 120.50
Output from the report example
Creating a Custom PrintDocument Class The generic PrintDocument class is easy to use, but has shortcomings with regard to data encapsulation. In the preceding example, it is necessary to declare variables that have class-wide scope—such as the StreamReader—to make them available to the various methods that handle PrintDocument events. A better solution is to derive a custom PrintDocument class that accepts parameters and uses properties and private fields to encapsulate information about line height, fonts, and the data source. Listing 9-4 shows the code from the preceding example rewritten to support a derived PrintDocument class. Creating a custom PrintDocument class turns out to be a simple and straightforward procedure. The first step is to create a class that inherits from PrintDocument. Then, private variables are defined that support the fonts and title that are now exposed as properties. Finally, the derived class overrides the OnBeginPrint, OnEndPrint, and OnPrintPage methods of the base PrintDocument class. The overhead required before printing the report is reduced to creating the new ReportPrintDocument object and assigning property values. string myTitle = "Acme Furniture: Inventory Report"; ReportPrintDocument rpd = new ReportPrintDocument(myTitle); rpd.TitleFont = new Font("Arial",10, FontStyle.Bold); rpd.ReportFont = new Font("Arial",10); PrintPreviewDialog prevDialog = new PrintPreviewDialog();
9.3 Printing
prevDialog.Document = rpd; prevDialog.ShowDialog(); // Preview Report // Show Print Dialog and print report PrintDialog pDialog = new PrintDialog(); pDialog.Document = rpd; if (pDialog.ShowDialog() == DialogResult.OK) { rpd.Print(); }
The preceding code takes advantage of the new constructor to pass in the title when the object is created. It also sets the two fonts used in the report.
Listing 9-4
Creating a Custom PrintDocument Class
// Derived Print Document Class public class ReportPrintDocument: PrintDocument { private Font hdrFont; private Font rptFont; private string title; private StreamReader sr; private float lineHeight; // Constructors public ReportPrintDocument() {} public ReportPrintDocument(string myTitle) { title = myTitle; } // Property to contain report title public string ReportTitle { get {return title;} set {title = value;} } // Fonts are exposed as properties public Font TitleFont { get {return hdrFont;} set {hdrFont = value;} }
455
456
Chapter 9
■
Listing 9-4
Fonts, Text, and Printing
Creating a Custom PrintDocument Class (continued)
public Font ReportFont { get {return rptFont;} set {rptFont = value;} } // BeginPrint event handler protected override void OnBeginPrint(PrintEventArgs e) { base.OnBeginPrint(e); // Assign Default Fonts if none selected if (TitleFont == null) { TitleFont = new Font("Arial",10,FontStyle.Bold); ReportFont = new Font("Arial",10); } / Code to create StreamReader or other data source // goes here ... sr = new StreamReader(inputFile); } protected override void OnEndPrint(PrintEventArgs e) { base.OnEndPrint(e); TitleFont.Dispose(); ReportFont.Dispose(); sr.Close(); } // Print Page event handler protected override void OnPrintPage(PrintPageEventArgs e) { base.OnPrintPage(e); // Remainder of code for this class is same as in // Listing 9-3 for Inven_Report, PrintDetail, and // PrintHdr } }
This example is easily extended to include page numbering and footers. For frequent reporting needs, it may be worth the effort to create a generic report generator that includes user selectable data source, column headers, and column totaling options.
9.4 Summary
9.4
Summary
This chapter has focused on using the GDI+ library to display and print text. The first section explained how to create and use font families and font classes. The emphasis was on how to construct fonts and understand the elements that comprise a font by looking at font metrics. After a font has been created, the Graphics.DrawString method is used to draw a text string to a display or printer. Its many overloads permit text to be drawn at specific coordinates or within a rectangular area. By default, text printed in a rectangle is left justified and wrapped to the next line when it hits the bounds of the rectangle. This default formatting can be overridden by passing a StringFormat object to the DrawString method. The StringFormat class is the key to .NET text formatting. It is used to justify text, specify how text is truncated, and set tab stops for creating columnar output. GDI+ provides several classes designed to support printing to a printer. These include the following: •
• •
•
PrintDocument. Sends output to the printer. Its Print method initiates the printing process and triggers the BeginPrint, QueryPageSettingsEvent, PrintPage, and EndPrint events. The event handler for the PrintPage event contains the logic for performing the actual printing. PrintPreviewDialog. Enables output to be previewed before printing. PrinterSettings. Has properties that specify the page range to be printed, the list of available printers, and the name of the target printer. These values correspond to those a user can select on the PrintDialog dialog box. DefaultPageSettings. Has properties that set the bounds of a page, the orientation (landscape or portrait), the margins, and the paper size. These values correspond to those selected on the PageSetupDialog dialog box.
An example for printing a columnar report demonstrated how these classes can be combined to create an application that provides basic report writing. As a final example, we illustrated how the shortcomings of the PrintDocument class can be overcome by creating a custom PrintDocument class that preserves data encapsulation.
457
458
Chapter 9
9.5
■
Fonts, Text, and Printing
Test Your Understanding
1. Does Arial Bold refer to a font family, typeface, or font? 2. What is the default unit of measurement for a font? What size is it (in inches)? 3. Which method and enumeration are used to right justify an output string? 4. When the following code is executed, what is the x coordinate where the third column begins? float[] tStops = {50f, 60f, 200f, 40f}; StringFormat sf = new StringFormat(); sf.SetTabStops(0,tStops); string hdr = "Col1\tCol2\tCol3\tCol4"; g.DrawString(hdr, myFont, Brushes.Black, 10,10,sf);
5. Which PrintDocument event is called after PrintDocument.Print is executed? 6. Which class available to the PrintPage event handler defines the margins for a page? 7. What three steps must be included to permit a document to be previewed before printing?
This page intentionally left blank
WORKING WITH XML IN .NET
Topics in This Chapter • Introduction to Using XML: Introduces some of the basic concepts of working with XML. These include the XML validation and the use of an XML style sheet. • Reading XML Data: Explains how to use the .NET XML stack to access XML data. The XmlReader, XmlNodeReader, XmlTextReader are examined. • Writing XML Data: The easiest way to create XML data is to use the .NET XmlSerializer to serialize data into the XML format. When the data is not in a format that can be serialized, an alternative is the XmlWriter class. • Searching and Updating XML Documents: XPath is a query language to search XML documents. Examples illustrate how to use it to search an XmlDocument, XmlDataDocument, and XPathDocument.
10
Extensible Markup Language (XML) plays a key role in the .NET universe. Configuration files that govern an application or Web page’s behavior are deployed in XML; objects are stored or streamed across the Internet by serializing them into an XML representation; Web Services intercommunication is based on XML; and as we see in Chapter 11, “ADO.NET,” .NET methods support the interchange of data between an XML and relational data table format. XML describes data as a combination of markup language and content that is analogous to the way HTML describes a Web page. Its flexibility permits it to easily represent flat, relational, or hierarchical data. To support one of its design goals— that it “should be human-legible and reasonably clear”1—it is represented in a text-only format. This gives it the significant advantage of being platform independent, which has made it the de facto standard for transmitting data over the Internet. This chapter focuses on pure XML and the classes that reside in the System.Xml namespace hierarchy. It begins with basic background information on XML: how schemas are used to validate XML data and how style sheets are used to alter the way XML is displayed. The remaining sections present the .NET classes that are used to read, write, update, and search XML documents. If you are unfamiliar with .NET XML, you may surprised how quickly you become comfortable with reading and searching XML data. Extracting information from even a complex XML structure is
1. W3C Extensible Markup Language (XML), 1.0 (Third Edition), http://www.w3.org/TR/REC-xml/
461
462
Chapter 10
■
Working with XML in .NET
refreshingly easy with the XPath query language—and far less tedious than the original search techniques that required traversing each node of an XML tree. In many ways, it is now as easy to work with XML as it is to work with relational data.
10.1 Working with XML Being literate in one’s spoken language is defined as having the basic ability to read and write that language. In XML, functional literacy embraces more than reading and writing XML data. In addition to the XML data document, there is an XML Schema document (.xsd) that is used to validate the content and structure of an XML document. If the XML data is to be displayed or transformed, one or more XML style sheets (.xsl) can be used to define the transformation. Thus, we can define our own form of XML literacy as the ability to do five things: 1. 2. 3. 4. 5.
Create an XML file. Read and query an XML file. Create an XML Schema document. Use an XML Schema document to validate XML data. Create and use an XML style sheet to transform XML data.
The purpose of this section is to introduce XML concepts and terminology, as well as some .NET techniques for performing the preceding tasks. Of the five tasks, all are covered in this section, with the exception of reading and querying XML data, which is presented in later sections.
Using XML Serialization to Create XML Data As discussed in Chapter 4, “Working with Objects in C#,” serialization is a convenient way to store objects so they can later be deserialized into the original objects. If the natural state of your data allows it to be represented as objects, or if your application already has it represented as objects, XML serialization often offers a good choice for converting it into an XML format. However, there are some restrictions to keep in mind when applying XML serialization to a class: • • • •
The class must contain a public default (parameterless) constructor. Only a public property or field can be serialized. A read-only property cannot be serialized. To serialize the objects in a custom collection class, the class must derive from the System.Collections.CollectionBase class and include an indexer. The easiest way to serialize multiple objects is usually to place them in a strongly typed array.
10.1 Working with XML
An Example Using the XmlSerializer Class Listing 10-1 shows the XML file that we’re going to use for further examples in this section. It was created by serializing instances of the class shown in Listing 10-2.
Listing 10-1
Sample XML File
5 Citizen Kane 1941 Orson Welles Y 1
6 Casablanca 1942 Michael Curtiz Y 1
In comparing Listings 10-1 and 10-2, it should be obvious that the XML elements are a direct rendering of the public properties defined for the movies class. The only exceptional feature in the code is the XmlElement attribute, which will be discussed shortly.
Listing 10-2
Using XmlSerializer to Create an XML File
using System.Xml; using System.Xml.Serialization; // other code here ... public class movies { public movies() // Parameterless constructor is required { } public movies(int ID, string title, string dir,string pic, int yr, int movierank) {
463
464
Chapter 10
■
Listing 10-2
Working with XML in .NET
Using XmlSerializer to Create an XML File (continued)
movieID = ID; movie_Director = dir; bestPicture = pic; rank = movierank; movie_Title = title; movie_Year = yr; } // Public properties that are serialized public int movieID { get { return mID; } set { mID = value; } } public string movie_Title { get { return mTitle; } set { mTitle = value; } } public int movie_Year { get { return mYear; } set { mYear = value; } } public string movie_Director { get { return mDirector; } set { mDirector = value; } } public string bestPicture { get { return mbestPicture; } set { mbestPicture = value; } } [XmlElement("AFIRank")] public int rank { get { return mAFIRank; } set { mAFIRank = value; } } private int mID; private string mTitle; private int mYear; private string mDirector; private string mbestPicture; private int mAFIRank; }
10.1 Working with XML
To transform the class in Listing 10-2 to the XML in Listing 10-1, we follow the three steps shown in the code that follows. First, the objects to be serialized are created and stored in an array. Second, an XmlSerializer object is created. Its constructor (one of many constructor overloads) takes the object type it is serializing as the first parameter and an attribute as the second. The attribute enables us to assign “films” as the name of the root element in the XML output. The final step is to execute the XmlSerializer.Serialize method to send the serialized output to a selected stream—a file in this case. // (1) Create array of objects to be serialized movies[] films = {new movies(5,"Citizen Kane","Orson Welles", "Y", 1941,1 ), new movies(6,"Casablanca","Michael Curtiz", "Y", 1942,2)}; // (2) Create serializer // This attribute is used to assign name to XML root element XmlRootAttribute xRoot = new XmlRootAttribute(); xRoot.ElementName = "films"; xRoot.Namespace = "http://www.corecsharp.net"; xRoot.IsNullable = true; // Specify that an array of movies types is to be serialized XmlSerializer xSerial = new XmlSerializer(typeof(movies[]), xRoot); string filename=@"c:\oscarwinners.xml"; // (3) Stream to write XML into TextWriter writer = new StreamWriter(filename); xSerial.Serialize(writer,films);
Serialization Attributes By default, the elements created from a class take the name of the property they represent. For example, the movie_Title property is serialized as a element. However, there is a set of serialization attributes that can be used to override the default serialization results. Listing 10-2 includes an XmlElement attribute whose purpose is to assign a name to the XML element that is different than that of the corresponding property or field. In this case, the rank property name is replaced with AFIRank in the XML. There are more than a dozen serialization attributes. Here are some other commonly used ones: XmlAttribute
Is attached to a property or field and causes it to be rendered as an attribute within an element. Example: XmlAttribute("movieID")] Result:
465
466
Chapter 10
■
Working with XML in .NET
XmlIgnore
Causes the field or property to be excluded from the XML.
XmlText
Causes the value of the field or property to be rendered as text. No elements are created for the member name. Example: [XmlText] public string movie_Title{
Result: Citizen Kane
XML Schema Definition (XSD) The XML Schema Definition document is an XML file that is used to validate the contents of another XML document. The schema is essentially a template that defines in detail what is permitted in an associated XML document. Its role is similar to that of the BNF (Backus-Naur Form) notation that defines a language’s syntax for a compiler. .NET provides several ways (others are included in Chapter 11, “ADO.NET”) to create a schema from an XML data document. One of the easiest ways is to use the XML Schema Definition tool (Xsd.exe). Simply run it from a command line and specify the XML file for which it is to produce a schema: C:/ xsd.exe
oscarwinners.xml
The output, oscarwinners.xsd, is shown in Listing 10-3.
Listing 10-3
XML Schema to Apply Against XML in Listing 10-1
10.1 Working with XML
Listing 10-3
XML Schema to Apply Against XML in Listing 10-1 (continued)
As should be evident from this small sample, the XML Schema language has a rather complex syntax. Those interested in all of its details can find them at the URL shown in the first line of the schema. For those with a more casual interest, the most important thing to note is that the heart of the document is a description of the valid types that may be contained in the XML data that the schema describes. In addition to the string and int types shown here, other supported types include boolean, double, float, dateTime, and hexBinary. The types specified in the schema are designated as simple or complex. The complextype element defines any node that has children or an attribute; the simpletype has no attribute or child. You’ll encounter many schemas where the simple types are defined at the beginning of the schema, and complex types are later defined as a combination of simple types.
XML Schema Validation A schema is used by a validator to check an XML document for conformance to the layout and content defined by the schema. .NET implements validation as a read and check process. As a class iterates through each node in an XML tree, the node is validated. Listing 10-4 illustrates how the XmlValidatingReader class performs this operation. First, an XmlTextReader is created to stream through the nodes in the data document. It is passed as an argument to the constructor for the XmlValidatingReader. Then, the ValidationType property is set to indicate a schema will be used for validation. This property can also be set to XDR or DTD to support older validation schemas.
467
468
Chapter 10
■
Working with XML in .NET
The next step is to add the schema that will be used for validating to the reader’s schema collection. Finally, the XmlValidatingReader is used to read the stream of XML nodes. Exception handling is used to display any validation error that occurs.
Listing 10-4
XML Schema Validation
private static bool ValidateSchema(string xml, string xsd) { // Parameters: XML document and schemas // (1) Create a validating reader XmlTextReader tr = new XmlTextReader(xml"); XmlValidatingReader xvr = new XmlValidatingReader(tr); // (2) Indicate schema validation xvr.ValidationType= ValidationType.Schema; // (3) Add schema to be used for validation xvr.Schemas.Add(null, xsd); try { Console.WriteLine("Validating: "); // Loop through all elements in XML document while(xvr.Read()) { Console.Write("."); } }catch (Exception ex) { Console.WriteLine( "\n{0}",ex.Message); return false;} return true; }
Note that the XmlValidatingReader class implements the XmlReader class underneath. We’ll demonstrate using XmlReader to perform validation in the next section. In fact, in most cases, XmlReader (.NET 2.0 implmentation) now makes XmlValidatingReader obsolete.
Using an XML Style Sheet A style sheet is a document that describes how to transform raw XML data into a different format. The mechanism that performs the transformation is referred to as an XSLT (Extensible Style Language Transformation) processor. Figure 10-1 illustrates the process: The XSLT processor takes as input the XML document to be transformed and the XSL document that defines the transformation to be applied. This approach permits output to be generated dynamically in a variety of formats. These include XML, HTML or ASPX for a Web page, and a PDF document.
10.1 Working with XML
XSL Document
XML Document
XML HTML ASPX PDF
XSL Transformation
Figure 10-1 Publishing documents with XSLT
The XslTransform Class The .NET version of the XSLT processor is the XslTransform class found in the System.Xml.Xsl namespace. To demonstrate its use, we’ll transform our XML movie data into an HTML file for display by a browser (see Figure 10-2).
Movie Title Casablanca Citizen Kane
Movie Year
AFI Rank
1942 1941
2 1
Director Michael Curtiz Orson Welles
Figure 10-2 XML data is transformed into this HTML output
Before the XslTransform class can be applied, an XSLT style sheet that describes the transformation must be created. Listing 10-5 contains the style sheet that will be used. As you can see, it is a mixture of HTML markup, XSL elements, and XSL commands that displays rows of movie information with three columns. The XSL elements and functions are the key to the transformation. When the XSL style sheet is processed, the XSL elements are replaced with the data from the original XML document.
469
470
Chapter 10
■
Listing 10-5
Working with XML in .NET
XML Style Sheet to Create HTML Output
Movies
Movie Title | Movie Year | AFI Rank | Director |
---|---|---|---|