223 82 4MB
English Pages [547] Year 1997
ActiveX Programming with Visual C++ Table of Contents: ●
Introduction
Part I: Introduction to ActiveX ● ●
Chapter 1 - What is ActiveX Chapter 2 - What Can ActiveX Do for You?
Part II: ActiveX Automation Servers ● ● ●
Chapter 3 - Creating ActiveX Automation Servers Using MFC Chapter 4 - Understanding Variable Typing, Naming, and Scoping Chapter 5 - Working with Program Flow and Control Structures
Part III: ActiveX Controls ● ● ● ● ● ●
Chapter 6 - Managing States and Events with Application and Session Objects Chapter 7 - Building a Foundation of Interactivity with Request and Response Objects Chapter 8 - Enhancing Interactivity with Cookies, Headers, and the Server Object Chapter 9 - Interactivity Through Bundled Active Server Components Chapter 10 - Constructing Your Own Server Components Chapter 11 - Constructing Your Own Server Components
Part IV: COM Objects and Custom Interfaces ● ● ●
Chapter 12 - Introducing ActiveX Data Objects Chapter 13 - Working with ADO's Connection and Command Objects Chapter 14 - Working with ADO's RecordSet Object
Part V: Using Your Components ● ●
Chapter 15 - Testing and Using Your Components Chapter 16 - Advanced Topics
To order books from QUE, call us at 800-716-0044 or 317-361-5400. For comments or technical support for our books and software, select Talk to Us. © 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.
ActiveX Programming with Visual C++ Introduction ActiveX development is at the brink of an amazing explosion of possibilities. If you participate at all on the Internet newsgroups or mail lists, you know what we mean. The number of developers doing ActiveX development has increased dramatically. The type of development and the level of sophistication that applications are supporting are incredible. All the work that Microsoft has done over the past five plus years, starting with OLE 2, is beginning to pay off. I remember sitting in an auditorium five years ago listening to a two and one-half hour lecture on OLE Automation and thinking, "Great, just what we need . . . another macro language!," and also thinking that I had no clue what OLE was going to do for development and the Windows platform. Well OLE 2 took all of us to new levels in the development of integrated components. With the advent of ActiveX, the future is looking brighter than ever. This book is devoted to bringing that technology down to earth and into your hands. After reading this book, you should feel confident that you can tackle any project or application involving ActiveX, COM, and Windows.
You and This Book When writing this book the question "Who is the target audience?" often came up. We didn't want to answer that question by saying, "It's for the beginner ActiveX developer," and then produce a "cookie cutter" copy of every other book on the market simply because it was safe or easy to do so. We also didn't want to say, "This book is only for advanced programmers," because that would exclude a lot of developers. Not because we feel you aren't able to understand the material, but because we would have to assume so much about your particular experience and knowledge and omit a lot of basic information just to squeeze everything into a book of this size. We also didn't want to lose anyone or make the book so hard to read that no one would buy it. The target audience for the book is the intermediate to experienced Windows VC++ developer. We do make some assumptions about your knowledge of basic ActiveX/OLE/COM architecture and of how to use the VC++ development environment. For example, you should know what an ActiveX Automation Server is or at least have a pretty good idea what it is. You should know how to compile applications and how to use the VC++ IDE without having to search the manuals to find out how to "update your project dependencies," for example. In this book, we describe how to create ActiveX controls using MFC, ATL, and BaseCtl. The material is covered in a relatively small number of pages and advances fairly rapidly from the basic concepts to advanced features. For those readers who are not familiar with ActiveX development, we recommend
starting with MFC, since MFC hides a lot of the details from you. Move to ATL as your comfort level increases, and tackle BaseCtl as the last subject. We believe that this approach enables you to do one-to-one comparisons among the different tools and get a feel for how easy it is to perform a set of tasks or a particular implementation. For example, you can compare the size of the applications, speed of the applications, development effort, learning curve, and so on. You will also find that every topic is covered by every tool. Too many times, books focus on a single method of implementation, never affording you the opportunity to judge for yourself which tool makes the most sense for you. When we cover a section on how to create COM interfaces using MFC, you can rest assured that we also show you how to do the same with ATL and the BaseCtl. Finally we believe our approach provides abundant coverage. At this time, no other publication demonstrates how to create ActiveX components with all three tools, let alone with as much detail as we provide in this book. And probably most important, this book generates experience. If you walk away from this book thinking, "You know, I don't know everything about ActiveX, but with a little time, I can learn anything I want to about it," then we'll be happy.
What You Need to Use This Book All of the samples in this book were developed and tested by using Microsoft Visual C++ 5.0. While a majority of the samples and examples involve Microsoft products directly, MFC and ATL, you are not restricted in how you apply the fundamental principles of ActiveX development. You may use any tool and obtain similar results.
How This Book Is Organized This book is organized into five parts, with 16 chapters that cover a variety of aspects of ActiveX programming. By no stretch of the imagination do we imply that we cover all aspects of ActiveX development within this text. We cover only those topics that we believe are of the most importance when using ActiveX on a day-to-day basis. This book assumes that you are reading the chapters in order, as they do build upon the concepts, materials, and experiences gained from previous chapters. The following sections provide an overview for each of the five parts of the book.
Part I: Introduction to ActiveX Chapter 1 is an overview of ActiveX and what it means to you as a developer. Chapter 2 examines the different types of ActiveX components that can be created and the advantages and disadvantages of each. Chapter 2 also looks at the types of tools available for creating components and their relative strengths and weaknesses.
Part II: ActiveX Automation Servers Chapter 3 examines in detail the creation of ActiveX Automation Servers using MFC. The chapter starts with the most fundamental aspects of server creation involving methods and properties. It also covers how to expand the basic MFC framework in order to add new features such as shared objects and single instance servers. Chapter 4 explains how to use ATL to create Automation Servers, and, finally, Chapter 5 explores the ActiveX BaseCtl sample and how it, too, can be used to create Automation Servers.
Part III: ActiveX Controls Part III, like Part II, contains chapters on MFC, ATL, and BaseCtl. Chapters 6 and 7 explain how to create ActiveX controls using MFC. Chapter 6 looks at how to implement some of the basic features of controls, such as methods, properties, and events. Chapter 7 examines more advanced concepts such as asynchronous properties and Drag and Drop support. Chapters 8 and 9 explain how to use ATL to create controls, and Chapters 10 and 11 explain how to use the BaseCtl sample to create controls.
Part IV: COM Objects and Custom Interfaces Chapter 12 is a detailed description of creating COM objects and Custom Interfaces using MFC. This chapter along with Chapters 13 and 14 focus on how to create the most fundamental aspect of ActiveX development: interface development. Chapter 13 examines using ATL to create COM objects, and Chapter 14 shows how you can implement COM interfaces without a framework such as MFC and ATL.
Part V: Using Your Component and Advanced Topics Chapter 15 looks at the types of containers and testing applications that are available to you as an ActiveX developer so that you can use and test your newly created components. Chapter 16 introduces you to more advanced concepts of ActiveX, such as DCOM, the Internet, and OLE DB, to name a few. These two chapters are not full descriptions of the topics listed but merely brief explanations of the concepts involved and how you can exploit them.
Conventions Used in This Book To make this book easier to understand, the following conventions are used: Characters that you are asked to type are shown in bold font.
A word or phrase used for the first time appears in italic font and is usually accompanied by a definition. Bulleted lists are used for items that do not require a special order. Numbered lists are used when you need to follow items or steps in a particular sequence. This book also includes special Notes, Tips, and Cautions, which are set off, as follows:
NOTE: This is an example of a Note. Notes provide additional information about issues that you should consider in using the described features or point out specific areas of interest in a particular section of code.
Descriptive heads are added to code listings to help you easily find and use the sample code and the application files shown throughout this book. The sample code in this book can be found on Que's Web site located at http://www.quecorp.com/activexvc. Occasionally within code listings, lines of code are too long to fit on one line. When this occurs, the code is broken into two of more lines. Boldface font is used to emphasize some lines of code in listings. Some code fragments are included in the text. These fragments are set in monospace font, for example, #include "CPATLControl.h".
Parting Remark Writing this book has been a tremendous learning experience. ActiveX development is far and away one of today's most exciting aspects of PC and client/server development. The tool sets and applications have finally matured to the level that real progress and advances can be made in component and applications development. We truly hope that we can all look back in another five years and think, "Wow, look how far we've come since then. And, of course, look how much is still ahead of us ."
Chapter 1 What Is ActiveX? ●
What Is ActiveX? ❍ An Internet Strategy for Applications Development ❍ ActiveX, OLE, and the Internet ❍ Classifying ActiveX Components ■ Automation Servers ■ Automation Controllers ■ Controls ■ COM Objects ■ Documents ■ Containers ❍ From Here...
What Is ActiveX? ●
●
An Internet strategy ActiveX started out as an Internet strategy. It now covers all aspects of OLE/COM/Internet development. ActiveX development covers many topics ActiveX development is very broad in scope, covering Automation Servers, Controls, and COM Objects, to name a few.
The term ActiveX has become the battle cry of many developers and development organizations over the past year. On the opposite side of the coin, sales and marketing organizations have also rallied around this same nebulous term. Few people, however, can truly explain what the term means. This book is dedicated to explaining what ActiveX is and what it means to developers. We hope that you learn as much from reading this book as we did from writing it.
An Internet Strategy for Applications Development Microsoft first coined the term ActiveX at the Internet Professional Developers Conference (Internet PDC) in March 1996. ActiveX referred to the conference slogan "Activate the Internet" and was more a call-to-arms than a technology or architecture for developing applications.
At the time of the Internet PDC, Microsoft was going head-to-head with Netscape over control of the Internet Web browser market. The PDC demonstrated, though, that Microsoft was interested in much more than just the browser market. Microsoft demonstrated tools ranging from electronic store fronts to new OLE Controls to virtual reality chat software, and beyond. ActiveX is the new corporate slogan of Microsoft--similar to the term OLE in the early 1990s--and in a very short time, has come to mean much more than "Activate the Internet." ActiveX has become the all-encompassing term used to define everything from Web pages to OLE (Object Linking and Embedding) Controls. It has come to signify, on one hand, small, fast, reusable components that can get you hooked into all the latest technologies coming out of Microsoft, the Internet, and the industry. On the other hand, ActiveX represents Internet and applications integration strategies. These days, products and companies that don't have ActiveX and Internet somewhere in their nomenclature are considered, both internally and externally, as being behind the times. The reality is that trying to describe ActiveX is similar to trying to describe the color red. ActiveX is not a technology or even an architecture--it is a concept and a direction.
ActiveX, OLE, and the Internet ActiveX and OLE have become synonymous. What people once referred to as OLE Controls (OCXs) are now refered to as ActiveX Controls. OLE DocObjects are now ActiveX Documents. In some cases, entire documents on how to implement OLE technologies have been updated to be ActiveX technologies, and the only thing changed was the term OLE, which now reads as ActiveX. Although tremendous advances have been made and seemingly new technologies appear daily with regard to OLE and ActiveX, it is questionable whether the Internet was or is directly involved in many of these areas. The need for small, fast, reusable components (COM Objects) has been around for years. Distributed components (DCOM Objects) were first demonstrated several years ago at the OLE 2.0 PDC. The Visual Basic (VB) group played a major role in the enabling of ActiveX in its early days. The BaseCtl framework, which is included in the ActiveX SDK, was developed by the VB group to answer its need for small, lightweight Controls to improve VB application's load times. The only contribution the Internet had was in its need for a way to implement and publish Web pages. Practically every new feature labeled ActiveX can trace its roots back to a fundamental, global need for small, fast, reusable components, all of which started with OLE and COM. ActiveX was not meant to replace OLE, but simply to broaden it to include the Internet, intranet commercial and in-house applications development, and the tools used to develop them. Microsoft has published a number of documents regarding ActiveX development. The OC 96 specification defines how Controls should be developed to provide faster startup times and better drawing capabilities. It also describes which interfaces are required and which are optional. The "OLE Control and Control Container Guidelines" provide important information for Control and Container interaction. The Microsoft Web site has become a cornucopia of information for creating, using, and
deploying ActiveX components. In addition to the specific technologies for creating ActiveX components, Microsoft has set a standard for the use and integration of ActiveX components. Every product from VB to Microsoft Word to Java is inherently capable of using ActiveX components. Four years ago, it was almost impossible to find more than a handful of applications that were capable of integrating in such a relatively seamless fashion as is possible today. The next section looks at the specific types of ActiveX components that can be created and-- to be even more helpful--how and when they should be used.
Classifying ActiveX Components This book addresses the topic of ActiveX Component development. These components can be classified and broken into the following categories: Automation Servers Automation Controllers Controls COM Objects Documents Containers This book covers in detail only the development of ActiveX Automation Servers, Controls, and COM Objects. Automation Controllers, ActiveX Documents, and Containers entail too many interfaces and too much technology to be addressed in a book of this size.
Automation Servers Automation Servers are components that can be programmatically driven by other applications. An Automation Server contains at least one, and possibly more, IDispatch-based interfaces that other applications can create or connect to. An Automation Server may or may not contain User Interface (UI), depending on the nature and function of the Server. Automation Servers can be in-process (executing in the process space of the Controller), local (executing in its own process space), or remote (executing in a process space on another machine). The specific implementation of the server will, in some cases, define how and where the server will execute, but that is not guaranteed. A DLL can execute as either in-process, local or remote; an EXE can execute only locally or remotely.
NOTE: The fastest execution times are from Servers that are in-process to the Controllers using them. But remember that using an in-process Automation Server does not guarantee in-process performance. If an in-process Automation Server is created in
one process space and then handed to a Controller in another process space, the Server becomes local and suffers from the same performance degradation as a Local Server. See Part II of this book for more information on process spaces and their impact on Server performance.
Automation Controllers Automation Controllers are those applications that can use and manipulate Automation Servers. A good example of an Automation Controller is VB. With the VB programming language, you are able to create, use, and destroy Automation Servers as though they are an integral part of the language. An Automation Controller can be any type of application, DLL or EXE, and can access the Automation Server either in-process, locally, or remotely. Typically, the registry entries and the implementation of the Automation Server indicate which process space the server will execute in relation to the Controller.
Controls ActiveX Controls are equivalent to what is referred to as OLE Controls or OCXs. A typical Control consists of a UI representation both at design-time and runtime, a single IDispatch interface defining all of the methods and properties of the Control, and a single IConnectionPoint interface for the events that the Control can fire. In addition, the Control may have support for persistence across its execution lifetimes and support for various UI features, such as cut-and-paste and drag-and-drop features. Architecturally, a Control has a large number of COM interfaces that must be supported in order to take advantage of these features. With the release of the new OLE Control and ActiveX guidelines for Control development, a Control is no longer limited to the feature set defined in the preceding text. Rather, the developer can now choose to implement only those features that are most useful and interesting to users of the applications. The Control and Container guidelines published by Microsoft list all the interfaces and their specific requirements. You can find this information at the Microsoft Web site: http://www.microsoft.com. ActiveX Controls always execute in-process to the Container in which they reside. The extension of a Control is typically OCX, but in terms of execution models, it is nothing more than a standard windows DLL.
COM Objects COM Objects are similar in architecture to Automation Servers and Controllers. They contain one or more COM interfaces and probably little or no UI. These Objects, however, cannot be used by the typical Controller application the way Automation Servers can. The Controller must have specific
knowledge of the COM interface that it "talks" to in order to use the interface, which is not the case for Automation interfaces. The Windows 95 and NT operating systems contain hundreds of COM Objects and Custom interfaces as extensions to the operating systems for controlling everything from the appearance of the desktop to the rendering of 3-D images on the screen. COM Objects are a good way to organize a related set of functions and data, while still maintaining the needed high-speed performance of a DLL.
NOTE: Automation Servers can also benefit from COM interfaces. These servers are known as dual-interface Servers. The IDispatch interface of the Automation Server also has a companion COM interface describing the methods and properties of the Object. Automation Controllers such as VB can take advantage of these dual interfaces to provide even greater performance when using the Server. The one drawback to dualinterface Servers is that they are limited to the set of data types supported by OLE Automation when defining methods and properties.
Documents ActiveX Documents, or DocObjects as they were originally called, represent Objects that are more than a simple Control or Automation Server. A document can be anything from a spread- sheet to a complete invoice in an accounting application. Documents, like Controls, have UI and are hosted by a Container application. Microsoft Word and Excel are examples of ActiveX Document Servers, and the Microsoft Office Binder and Microsoft Internet Explorer are examples of ActiveX Document Containers. The ActiveX Document architecture is an extension of the OLE Linking and Embedding model and allows the document more control over the container in which it is being hosted. The most obvious change is how the menus are presented. A standard OLE Document's menu will merge with the Container, providing a combined feature set; whereas an ActiveX Document will take over the entire menu system, thus presenting the feature set of only the document and not that of both the Document and the Container. The fact that the feature set of the Document is exposed is the premise for all the differences between ActiveX Documents and OLE Documents. The Container is just a hosting mechanism, and the Document has all of the control. Another difference is printing and storage. An OLE Document is intended to be a part of the Containers Document that is hosting it and, thus, is printed and stored as a piece of the host Containers Document. ActiveX Documents are expected to support their native printing and storage functions and are not integrated with the Containers Document. ActiveX Documents are used within a uniform presentation architecture, rather than within an embedded document architecture, which is the basis for OLE Documents. Microsoft Internet Explorer is a perfect example of this. The Explorer merely presents the Web pages to the user, but they are viewed, printed, and stored as a single entity. Microsoft Word and Microsoft Excel are examples of
the OLE Document architecture. If an Excel spreadsheet is embedded in a Word document, the spreadsheet is actually stored with the Word document and is an integral part of it. ActiveX Documents also have the added capability of being published as Web pages on the Internet or on a corporate intranet. Imagine an in-house tracking system for purchase orders run from the same Web browsers that are used to connect to the Internet.
Containers ActiveX Containers are applications that can host Automation Servers, Controls, and Documents. VB and the ActiveX Control Pad are examples of Containers that can host Automation Servers and Controls. The Microsoft Office Binder and the Microsoft Internet Explorer can host Automation Servers, Controls, and Documents. With the decreasing requirements defined by the ActiveX Control and Document specifications, a Container must be robust enough to handle the cases where a Control or Document lacks certain interfaces. Container applications may allow little or no interaction with the Document or Control they host, or they may provide significant interaction capabilities in both manipulation and presentation of the hosted component. This capability, however, is dependent upon the Container hosting the component and is not defined by any of the Container guidelines as being required.
From Here... Chapter 2 takes a slightly more detailed look at the specific ActiveX and OLE technologies available and how best to apply them to your requirements.
Chapter 2 What Can ActiveX Do for You? ●
What Can ActiveX Do for You? ❍ Defining the Needs and Requirements of Your Application ❍ What Type of ActiveX Component Do You Need? ■ Automation Servers and Controllers ■ ActiveX Controls ■ COM Objects ❍ Selecting the Right Tool for the Right Job ■ Microsoft Foundation Classes ■ ActiveX Template Library ■ BaseControl Framework ■ Create Your Own Framework ❍ Basic ActiveX Component Architecture ■ ActiveX Automation Servers ■ ActiveX Controls ❍ Support Tools Needed for Building ActiveX Components ■ MIDL Compiler ■ Mktyplib ■ GUIDGEN ■ RegEdit ■ Registration Server ■ Ole2View ■ Adding the Tools to the Visual C++ Development Environment ❍ From Here...
What Can ActiveX Do for You? ●
●
Application requirements It is important to understand as many of the requirements as possible before starting your development project. Choosing the correct architecture and component type The various ways in which ActiveX components can be created are crucial to a successful project. You don't want to do too much or too little.
●
●
●
Choosing the correct tool The tool used to develop your component also affects the success of the project. Picking the right tool for the job is as important as is understanding the job itself. Basic ActiveX component architecture Each ActiveX Component type has its particular architecture and construction. Understanding this architecture is important. Basic ActiveX support tools You will find a collection of development support to tools that are invaluable to your ActiveX development.
Gone are the days of hacking together simple stand-alone applications that had all the interoperability of two semi-trailer trucks screaming toward each other at 100 miles an hour. With the advent of OLE (Object Linking and Embedding) and more recently ActiveX and the Internet, applications are expected to be flexible, modifiable, and extendible (with "clairvoyant" running a close fourth). Spending a little time up front working out the design and architecture of your application can and will make all the difference in the world. Almost any application worth its salt has resulted from some forethought, regardless of whether that information is crammed into the head of one of your developers, scribbled on restaurant napkins, or written in formal documentation. I recommend the latter two since removing your developer's head to take to a strategy meeting is probably not an option. The basic principles of OLE and ActiveX are going to determine most of the specific component architecture and design. For example, ActiveX Controls and Documents are developed within a specific set of parameters and rules so that they will interact with Containers correctly. Automation Servers and Controllers have to conform to OLE Automation rules. And COM Objects have to support the basic fundamentals of COM. But what about component relationships and lifetimes? What about access to interfaces and support for security? What if your component is going to be utilized by users who speak another language?
Defining the Needs and Requirements of Your Application A specification is important to establish the basic requirements of the component you are asked to create. Before you can proceed, you must have a clear understanding of what kind of component or application is needed and why you are creating it. Appropriate questions to ask are, "What created the need for the component, and how is it going to be used?" If the person or persons can't describe the problem, they probably don't understand the problem. The last thing anyone needs is an incomplete picture of the problem, which tends to create delays and promote last minute changes that can cause unexpected results. Try to get as much of the specification as possible on paper. After you determine the need, you can move on to designing the component. Again, it is critical to get
as much information as possible. Does the problem require a single component or multiple components? Do the components need the capability to interact together? And, if so, is speed an issue? What about the skill level of your developers? How are they able to cope with change or possibly new and unfamiliar development methods? What are the support and maintenance requirements? What is the problem? And what is going to be the solution? Take for example the need for creating inhouse purchase requests. Is it simple enough to say, "We will create an ActiveX Document and use our Web browsers to interact with the Document"? Probably not. What about e-mail integration? What about training for how to use the Web browser and the purchase request application? What if the purchase request application must integrate with a legacy application in-house? What if the development tool that you normally use can't create ActiveX Documents? What if there aren't any developers on staff who can handle the effort? Most of these issues can be boiled down to a single question, "What is the level of effort versus the amount of gain?" That is, is it worthwhile to pursue a development project that is difficult to understand, implement, and maintain? Or is it worthwhile to use a simpler approach and live with its limitations? All these issues and more will affect the kind of component you create and how you will develop it. As a developer of ActiveX components, it is your responsibility to know the answers to these questions. You have the specific domain knowledge, that is, the capabilities and limitations of yourself and the tools that you use. This book will hopefully aid you in making the correct choices when choosing and implementing a development plan.
What Type of ActiveX Component Do You Need? The first thing you must decide is what kind of component best fits your requirements.
Automation Servers and Controllers Automation Servers and Controllers probably have the greatest amount of flexibility. The Servers' IDispatch interface can be used from just about every major application available from Microsoft and hundreds of other manufacturers. Because it won't suffer from the same versioning requirements placed on strict COM interfaces, the interface also lends itself well to prototyping and modeling component interactions. A dual-interface Automation Server created in-process is the fastest type of Automation Server. Dualinterface refers to the fact that a Server contains two interfaces one based on IDispatch and the other based on COM. The COM interface is actually the fastest of the two. An in- process Server means that the application resides in the same memory address space as the application that created it. This allows the invocation of methods defined within the Server to be performed significantly faster because there is no burden of having to cross process boundaries every time a method is called. For the same reason, Server load times are fast because of the minimal number of steps involved in creating the Server and getting its IDispatch or COM interface pointer.
Nothing inherent to Automation architecture promotes the use of User Interface (UI), and nothing prevents its use either. You have complete freedom and control over how the Servers are implemented and used. Your Automation Servers could potentially contain UI in the form of a dialog or a form. Automation Servers also lend themselves well to the increasingly popular multitiered applications architectures that have appeared in recent years. The separation of UI from function is perfect for Automation Servers because you have complete freedom over how your Servers are implemented and used. Creating Servers with thin UI layers that utilize other Servers with no UI to accomplish a task is at the heart of multitiered applications development. Automation Servers should be used in the same fashion as any other DLL. The only difference between an Automation Server and a standard Windows DLL is the fact that the Server uses only a restricted set of data types whereas a DLL can use any type. Designing Automation Servers that work without the need for UI also makes them prime candidates for Distributed COM (DCOM) and other distributing technologies.
ActiveX Controls You should use ActiveX Controls primarily as they were intended to be used: as UI components to enhance or support a dialog, form, or document. Controls can be expensive to load because they can potentially require a large number of interfaces, depending on the functionality the Control supports. The OC 96 specification added the QuickActivate interface to help with Control load times, but the improvement was not significant. In addition, the OC 96 specification identified a number of interfaces that are considered optional or conditional, depending on the type of Control you are implementing. It is wise to review the specification to determine what can and cannot be removed from your implementation in order to improve its performance and overall size. When creating Controls, be sure to make them as lean as possible. If the Control will not be commercially distributed, remove the "AboutBox" code. Also, see whether you can get away with relying on the property editor of the application's development tool that will be used rather than supporting property pages. Avoid large amounts of persistence, and save the data only if you must. The real message here is to implement only those features that are truly useful and helpful for your Control implementation.
COM Objects COM Objects (Custom Interfaces) are far more flexible than any other component type when it comes to interface design. They are also the fastest interfaces in terms of execution times, although that is with the caveat that the COM interface is in-process to the application using it. COM Objects can use any data type within their interface definitions and do not suffer from the same restrictions as Automation Servers. This situation does present a problem when crossing process boundaries because the Object will then require its own proxy-stub marshaling code. Proxy-stub marshaling is what takes place when an application resides in a process space other than
the application it is communicating with. It is necessary to translate function calls and data to a context that can be understood by both applications. This is the responsibility of the proxy-stub code and is true for all types of OLE components. Going out-of-process with any type of component will have a profound effect on the performance of the application because a lot more work is taking place in order to perform the specific set of operations. Automation Servers rely on built-in proxy-stub marshaling code, whereas COM interfaces are required to create their own. This problem is not insurmountable, but it does add to the development time and effort, maintenance of the code, distribution of the Object, and overall performance of the application, so it needs to be considered when deciding on what type of component to develop. If you are going to go out-of-process with the COM Object, you should probably consider using an Automation Server because the performance will be comparable between the two, that is unless you are going to come up with your own marshaling code that significantly outperforms the built-in mechanisms. COM Objects are useful for the cases where the limited set of data types available to Automation Servers has a significant impact on the type of interface that can be created. An example of a COM Object might be a simple implementation that performs calculations of a large volume of user-defined data. Instead of copying the data and passing it to the COM Object, it might be more useful to pass a pointer to the data and allow the COM Object to manipulate the data directly. Automation Server data type restrictions would not allow for the creation of this kind of interface. COM does, however. Although in this case, the COM Object can execute only in-process because it needs direct access to the data.
Selecting the Right Tool for the Right Job Microsoft is going hog-wild with its tools development. Every product coming out these days seems to have the capability of building one kind of ActiveX component or another. Applications like Visual C++, Visual Basic (VB), J++, Access, FoxPro, Microsoft Word, and Microsoft Excel, just to name a few, can create anything from ActiveX COM Objects to ActiveX Documents. This book addresses creating ActiveX components by using Microsoft Visual C++ (VC++). Deciding whether to use VC++ for your development is usually based on one issue: limitations. All other products and tools capable of creating ActiveX components are going to suffer from some form of limitation. VC++ is the most powerful and flexible tool for creating ActiveX components, and now that you have decided VC++ is the way to go, you need to decide on a development strategy. When creating your ActiveX component with Visual C++, you have four options, which are all described in the following sections.
Microsoft Foundation Classes The Microsoft Foundation Class Library (MFC) is the easiest choice of all the tools available for ActiveX development. The VC++ IDE (Integrated Development Environment) is designed specifically with MFC in mind and provides very useful application and ClassWizards for developing
your application. MFC is robust and will probably cover 90 percent of your application's needs. Unfortunately, like every other software project that you have probably worked on, the last 10 percent is where you spend 90 percent of your time. Going outside the bounds of what MFC defines can be difficult and, in some cases, impossible. Take for example the requirement to have an Object that is single instance only. No matter how the Object is created by the client application, you always want the same instance returned. Providing this kind of functionality is impossible with MFC without modifying the built-in Class Factory classes, and these are not normally exposed to the developer. Supporting dual-interfaces in Automation Servers is not impossible, but it does cause enough changes in your code so that the ClassWizard can no longer be used to completely maintain your methods and properties. Some work will have to be done by hand. MFC does provide a number of features and functions when developing ActiveX components, but be prepared to live by its rules. Occasionally, you can bend the rules, but you can almost never break them. The following chapters discuss how to successfully bend the rules in MFC and implement both single instance and dual-interface servers. A good rule of thumb when working with MFC is to avoid using the built-in classes as much as possible by utilizing the basic Windows API instead. Avoiding use of the MFC classes to solve your application problems has two benefits. The first is that your application will generally run faster; the second is that moving to an alternative development tool such as ATL or BaseCtl will prevent a large amount of code rewrite. A large portion of the MFC classes have equivalent Windows API functions, especially in the area of GDI and drawing, and is not that much of a departure from MFC. Basic storage classes, such as lists and arrays, could be better provided by a general purpose class library, such as the Standard Template Library (STL), which can be used in combination with all of the ActiveX development frameworks you will be seeing in this book.
ActiveX Template Library ActiveX Template Library (ATL as it has come to be known), is a newcomer to the ActiveX arena. It first appeared in the summer of 1996 and quickly became a favorite among developers. Based on the amount of development taking place by using ATL and the fact that, unlike the BaseCtl framework, it is actually a supported product, Microsoft and the industry have obviously seen ATL as a viable platform for creating ActiveX components and it should be around for a long time. The initial implementation, versions 1.0 and 1.1, focused on the creation of small and fast Automation Servers and COM Objects. With the introduction of 2.0, ATL expanded its coverage to include ActiveX Controls and other ActiveX components. The level of integration with the VC++ IDE originally consisted only of an AppWizard used to create the basic ATL project, which, by the way, was more complete than its MFC counterpart. Also the ClassWizard could be used to maintain the Objects, methods, and properties as it can with MFC. ATL version 2.0 and VC++ 5.0 are now fully integrated, supplying the same level of tool support, such as AppWizards, ObjectWizards, and ClassWizards.
An added bonus to ATL is that it can be integrated into existing MFC applications without dire consequences or enormous amounts of work. This capability gives you complete freedom to develop your component without the restrictions that MFC imposes, while still being able to use nice MFC classes and features (like structures, arrays, and lists, to name a few).
BaseControl Framework BaseControl (BaseCtl) Framework and the ActiveX SDK is without a doubt the most difficult route to choose for ActiveX component development. The BaseCtl was first developed by the Visual Basic 4 (VB 4) development group in late 1995 and early 1996 in response to growing demands for better performance when using OCXs and VB. BaseCtl (then referred to as the "MarcWan" framework because of its primary developer, Marc Wandschnieder at Microsoft) was intended as a bare-bones framework to be used to create lightweight OLE Controls. In an effort to quell the demand for tools to create OLE Controls, the framework was put into the hands of various Control developers and vendors who were in contact with Microsoft and the VB group. At the Internet PDC, Microsoft packaged the BaseCtl Framework as part of the ActiveX SDK, and the rest, as they say, is history. The BaseCtl has no integration with the VC++ environment. In fact, the version of the BaseCtl framework that ships with the ActiveX SDK is little more than sample programs from which you can create new applications. Another version of the baseCtl framework that has been available to members of the VB 4 and 5 beta testers actually contains an AppWizard. The AppWizard used to create the base set of source files is written in VB and is ad hoc at best. The BaseCtl relies on a series of Object and library files that have to be built by you, the developer, before they can be used. All of the source files that come with the SDK and those generated by the AppWizard depend on commandline compilation. With a little bit of effort on your part, the projects can all be converted to VC++ projects, including the Object and library files that come with the SDK. The documentation for the BaseCtl is rudimentary and somewhat cryptic. Basic Control development with the BaseCtl framework can be difficult, as well. A fair number of the functions and capabilities that you're used to in MFC aren't present in the BaseCtl. A number of the function names are different, and the architecture for persistence is completely different. BaseCtl is meant to get the job done with as little code as possible, and unfortunately it's obvious. For those of you who have already written Controls in MFC and want to port them to the BaseCtl, I have only one thing to say, "Roll up your sleeves because it's going to get messy." With the BaseCtl, you're expected to dig into the guts of the framework and build a lot of the function yourself. One thing the BaseCtl has going for it is a fair number of samples. When installing the BaseCtl, it is recommended that you install the samples as well. Chances are that if you need to do something, it's in one of the samples. VC++ has a nice feature called "Find in Files." Take advantage of it. Another nice feature of the BaseCtl is the capability to access all the source code in the BaseCtl framework directly, so if you find a bug (and there are a couple), you can fix it yourself and move on.
Also, you have a lot more freedom to model your Control as you want. For example, you have two Controls that you want to develop; one is a Number Control, for basic numeric data input, and the other is a Currency Control, for basic currency data input. Both can rely heavily on the C++ inheritance model at the code and interface levels by creating a BaseNumeric Control. You don't have this kind of freedom with MFC. BaseCtl should not be taken lightly, and you can expect a lot of work when implementing a component with it. Even worse, the results may not justify the work. In one case, after converting an existing MFC Control to the BaseCtl, a 40 percent improvement was realized in the average load time of the two versions of the Control. You might think "Wow--40 percent! That's pretty good." Unfortunately, the load times were already so low for both Controls that you literally had to have hundreds of Controls on the form before the improvement was noticeable.
Create Your Own Framework The last method for Control development is to just sit down and do it. Get code from the class libraries, samples, books, and so on, and come up with your own framework, tools, or whatever you want. But you can expect the work to be hard and time-consuming. To get an idea of how much work is actually involved, stop for a minute and take a look at some of the source files in MFC, ATL, and the BaseCtl. Literally thousands of lines of code have been implemented over the course of months and even years. Due to the constantly changing nature of OLE and ActiveX requirements, it is wiser to choose an existing platform rather than trying to reinvent the wheel. The key to successful Control development is not in the framework that you choose to develop in, but in how you apply it.
Basic ActiveX Component Architecture Before moving on to the actual implementation of each type of ActiveX component, you need to review some of the basic concepts and architecture surrounding each component. Even though you can develop a wide variety of ActiveX components--controls, servers, documents, and so on--one thing is true for all of them: Underlying every component is the Component Object Model or COM architecture. COM defines the standard that all ActiveX components rely on when interacting with other ActiveX components. In addition to COM, all ActiveX components are further defined or restricted by the operating system in how they are created and used. The type of ActiveX component you create will further define or restrict your options when creating components. A wide variety of choices are available to you as a developer and it is important to understand the ramifications of each.
ActiveX Automation Servers
Probably the easiest to implement and most flexible form of ActiveX component is the Automation Server. An Automation Server is an application that contains one or more IDispatch-based interfaces. An interface is a collection of related methods and properties and an IDispatch interface is the name of the COM interface that is used to generically invoke those methods and properties. For more information on IDispatch interfaces please see the VC++ books online. The capability to define unique methods and properties for each server and have them be accessible through a generic mechanism is the real power of Automation Servers. An automation server may or may not be directly creatable by other applications via a CreateObject or similar call. It is possible to have what are referred to as nested objects that represent a hierarchy of objects. A single creatable automation object is responsible for the creation and distribution of other automation objects. For example, an application may expose a Document automation interface that can be created and manipulated by another application but that only exposes a Page interface as a method call to the document object. The lifetime of the Page object is less than or equal to the Document object and cannot exist on its own. (The terms object and server are used synonymously throughout this book.) Three of the chapters in this book focus on creating ActiveX Automation servers using MFC, ATL, and the BaseCtl framework. Each of the tools has its own set of strengths and weaknesses and, depending on your specific application requirements, will determine which tool you should use. MFC is great for rapid development and ease of modification. Servers created with MFC will be the largest and slowest of the three types. Deviating from the standard MFC implementation of Automation Servers can also be a limiting factor when using this tool. MFC's greatest strength is its integration with the VC++ IDE and the speed with which an implementation can be up and running. In only minutes developers can create a server and implement its methods and properties, assuming that they are familiar with the tools available. ATL by itself can create small, lightweight servers that are easy to use and modify. ATL is limited to the creation of COM-based objects only and is lacking in generic or utility class support. Combined with MFC and the Standard Template Library (STL), ATL is a very powerful framework for ActiveX development. For those developers who are willing to take the time to learn it, ATL is by far the best route. The BaseCtl also allows you to create small, lightweight servers. Unfortunately, the BaseCtl lacks support from Microsoft as a product which impacts its capability to be a practical long-term development tool. It has, however, a very straightforward and easy-to-understand architecture that is helpful to those developers who are learning OLE and ActiveX from the ground up and who are not afraid of all the gory details. Types of Automation Servers When creating an Automation Server, you must decide how to implement the server relative to how it is going to be used. You can create two basic types of servers: DLL-based and EXE-based. DLL-based Servers If the server does not have to run as a stand-alone application and performance is a critical issue, you should implement your server as a DLL. A DLL-based server is typically referred
to as an in-process server because of how it normally executes relative to its controller. EXE-based Servers If the server application does have a requirement to run as a stand-alone application, you must implement it as an EXE. An EXE-based Automation Server is typically referred to as a local Server or out-of-process Server, and it will execute in its own process space. Types of Server Execution Automation Servers also have an execution model that is independent of how the server is written. Automation Servers can execute either in-process, locally or remotely, relative to the application that has invoked or is using it. In-Process Execution An in-process Server is called in-process because it executes within the same process space as that of the application that created it. Only DLL-based automation Servers can execute as in-process, but that is not a guarantee. This is very important to note when using nested objects or shared objects. If an object is created in a process space, say Process A, and it is passed to another application in another process space, Process B, the Server in Process A will execute as a local Server relative to the application in Process B regardless of the fact that the Server in Process A is a DLL-based Server. This issue is very critical since more times than not in-process Servers are used to improve performance of the application using them. Local Execution Local execution is when an Automation Server is executing in a process space other than the process space of the controller application. As was stated earlier, a DLL-based Server may execute locally to its controller, depending on how it was created versus which application is using it. The main issue with local Servers is performance since all of the method calls have to cross process boundaries. This condition requires additional code overhead to move data back and forth between the Server and its Controller. Remote Execution Remote Execution is when the Server is executing on a machine other than the application that is controlling it. As with local Servers, performance is an issue with this type of execution.
ActiveX Controls An ActiveX Control, for all intents and purposes, is still the same OCX or OLE Control that you have come to know and love over the past several years. In fact, the only change with the coming of ActiveX is a decrease in the requirements to qualify as a control. To qualify as an ActiveX Control, a component must be a COM Object, implement an IUnknown interface, and support registration and unregistration through the exported functions, DLLRegisterServer and DLLUnregisterServer. That's it! Pretty easy, right? Well, not really. Even though your component qualifies as an ActiveX control, if all it supports is the preceding features, it will not do much more than take up space on your hard disk. If it needs UI, persistence, events, or any other feature common to controls, the control must implement other categories of interfaces. The exact requirements are in the OLE Control and Control Container Guidelines, Version 1.1 published by Microsoft. All of the guidelines for ActiveX development are available on the
Internet at the Microsoft Web site or on the ActiveX SDK CD. You have three tools at your disposal for creating ActiveX controls: MFC, ATL, and BaseCtl. As we pointed out earlier in the section "ActiveX Automation Servers," each tool has its strengths and weaknesses. With ActiveX Controls you only have one option when creating and executing the control: as a DLL and in-process to the Control's container application. Even though the extension of the Control is .ocx, it is still in fact just a .dll.
Support Tools Needed for Building ActiveX Components When creating your ActiveX Components, a few tools are essential to successful ActiveX development. Most of these tools are automatically installed as part of the Visual C++ development environment. As ActiveX development matures, more and more tools will become available. Using as many tools as you can will greatly improve your understanding of how your components work, as well as improve their overall implementation.
MIDL Compiler The Microsoft MIDL compiler is now a standard component of the Microsoft Visual C++ environment. The MIDL compiler compiles COM interface definitions (IDL) files into C code, which is then compiled into the project by the Visual C++ compiler. The MIDL compiler also provides support for marshaling interfaces across process boundaries. Starting with Visual C++ 4.0, the MIDL compiler started shipping as a standard component of Visual C++. The MIDL compiler is also available via the Win32 SDK from Microsoft.
Mktyplib Mktyplib is a type library compiler for compiling ODL files. Mktyplib is the predecessor to MIDL and produces the same type of output as MIDL. The use of Mktyplib will decrease as Microsoft moves from ODL to IDL files.
GUIDGEN GUIDGEN is a tool that is used to generate Global Unique Identifiers (GUID), which can be used for Interface IDs, Class IDs or any other 128-bit UUID, such as an RPC interface. GUIDGEN is installed only when the OLE development option is selected during the Visual C++ installation. When GUIDGEN is run, a GUID is created and placed in the Windows clipboard. After running the GUIDGEN application, the resultant GUID is pasted from the clipboard into the code that needs a GUID.
RegEdit RegEdit, or the registration editor, is a standard component of both the Windows 95 and Windows NT operating systems. The registration editor is used for browsing and altering operating system and application settings. The registration editor can also be used for installing and registering your COM objects.
CAUTION: RegEdit is a very powerful tool and must be used with extreme caution by experienced users. If used improperly, systems can be damaged, resulting in a loss of data or a malfunctioning computer.
In Windows 95, this program is called regedit.exe. In Windows NT, this program is called regedt32.exe.
Registration Server The Registration Server is an application that can be used to register the settings of a COM object in the Windows registry without the need to create a separate registration file. The application is called regsvr32.exe and is automatically installed if the OLE development option is selected during Visual C++ installation or if the ActiveX SDK is installed.
Ole2View Ole2View is a program that lists ActiveX components relative to their function and category. Ole2View is extremely helpful in the test and use of your components. Ole2View can be used to determine whether your component is registered properly and the type and number of interfaces your component defines. It even allows you to instantiate and access various components and interfaces within your application, and now it has expanded support for configuring components to use DCOM. When attempting to determine what is the cause of problems with a troublesome component, Ole2View is invaluable.
Adding the Tools to the Visual C++ Development Environment In order to maximize development productivity, the tools needed for COM programming should be integrated into the Visual C++ environment. Each of the tools needed can be added to the IDE's (Integrated Development Environment) Tools menu. The following section illustrates how to incorporate the tools into the IDE. Adding GUIDGEN to the Visual C++ environment enables the generation of a UUID from a single menu command. The generated UUID is placed in the Windows clipboard and must be pasted into the
project code. To add GUIDGEN to the Visual C++ environment: 1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog. 2. In the Menu Contents list box, scroll to the bottom of the list, and select a blank line, and type &Generate New UUID. 3. In the Command edit box, type GUIDGEN.EXE. 4. Clear all text from the Arguments edit box. 5. Clear all text from the Initial Directory edit box. 6. Click the Close button to add the entry to the Tools menu. In addition to the GUIDGEN program, you may want to consider adding the registry editor, the ability to unregister a component, and any other tool that you find useful for your development.
From Here... All of the frameworks described in this chapter have their strengths and weaknesses. MFC allows for rapid component creation and implementation and the level of support built into the VC++ IDE for MFC is beyond comparison with ATL or BaseCtl. MFC offers a very large and robust class library for solving most, if not all, of your development problems. MFC does, however, suffer from the problem that it is everything to everyone, which results in a slower application or one that cannot deviate from the "norm" fairly easily. ATL provides a small and deliberate framework for creating ActiveX components. ATL, however, falls very short in the area of common class and utility support, which is the thing that is MFC's strength. In addition, ATL's integration with the VC++ IDE also leaves room for improvement. BaseCtl is similar to ATL in that it is focused specifically on small, fast component development. Like ATL, it lacks the same common class and utility support that makes MFC so attractive. BaseCtl has an added negative of being considered only as a sample and not as a supported product by Microsoft. The level of experience of the development team and the intended life cycle of the code and applications will also affect the decision of which tool to choose to create ActiveX components. Take the time to investigate all of the options available to you before deciding on a platform and a direction.
The following chapters examine in detail how to implement some of the components described so far using MFC, ATL, and BaseCtl. Even with all of the information presented so far, the only true way to know that you have made the right choice is to do it yourself.
Chapter 3 ●
Creating ActiveX Automation Servers Using MFC ❍ Creating the Basic Project ❍ Adding an Automation Interface to the Application ■ Listing 3.1 MFCSERVER.ODL--Dispinterface and CoClass ODL Entries ■ Listing 3.2 TRACKER.H--Add the Class Factory Support with the Macro DECLARE_OLECREATE ■ Listing 3.3 TRACKER.CPP--Add the Class Factory Implementationwith the IMPLEMENT_OLECREATE Macro ❍ Registry ■ Server Registration ■ Server Unregistration ❍ Sample Server Support Code ■ Listing 3.4 TRACKER.H-- Sample Server Support Code Added to the Header File ■ Listing 3.5 TRACKER.CPP--Updated Source File ❍ Adding Methods ■ Listing 3.6 MFCSERVER.ODL--Updated ODL Entry for OutputLines Method ■ Listing 3.7 TRACKER.H--New Member Variable Added to the Tracker Class ■ Listing 3.8 TRACKER.CPP--Member Initialization in the Constructor ■ Listing 3.9 TRACKER.CPP--OutputLines Method Implementation ❍ Adding Properties ■ Listing 3.10 TRACKER.CPP--Indent Property Implementation ❍ Generating OLE Exceptions ■ Listing 3.11 MFCSERVER.ODL--Error Enumeration ■ Listing 3.12 TRACKERERROR.H--Tracker Error Constants ■ Listing 3.13 TRACKER.CPP--Exception Handling Code Added to the Source Files ❍ Dual-Interface ■ Listing 3.14 MFCSERVER.ODL--ODL Changes to Support Dual-Interface ■ Listing 3.15 TRACKER.CPP--ODL-Generated Header File Is Added to the Tracker Source File ■ Listing 3.16 TRACKER.H--Interface Macro Update of the Tracker Class Definition ■ Listing 3.17 TRACKER.CPP--Interface Implementation of the ITracker Interface ■ Listing 3.18 TRACKER.CPP--IDispatch Function Implementation for a Dual-Interface Server ■ Listing 3.19 TRACKER.CPP--ITracker Function Implementation ❍ Generating Dual-Interface OLE Exceptions ■ Listing 3.20 ERRORINFOMACROS.H--ISupportErrorInfo Helper Macros ■ Listing 3.21 TRACKER.CPP--ISupportErrorInfo Include File ■ Listing 3.22 TRACKER.H--ISupportErrorInfo Class Declaration ■ Listing 3.23 TRACKER.CPP--ISupportErrorInfo Interface Implementation ■ Listing 3.24 TRACKER.CPP--Custom Interface Exception Handling Code ❍ Server Instantiation Using C++ ❍ Shared Servers ■ Listing 3.25 TRACKER.H--Shared Object Member Variable Added to the CTracker Class ■ Listing 3.26 TRACKER.CPP--CLSID Declaration
Listing 3.27 TRACKER.CPP--RegisterActiveObject Added to the CTracker Constructor ■ Listing 3.28 TRACKER.CPP--RevokeActiveObject Added to the Server Single Instance Servers ■ Listing 3.29 SHAREDOBJECT.H--Shared Server Class Factory Header File ■ Listing 3.30 SHAREDOBJECT.CPP--Shared Server Implementation File ■ Listing 3.31 TRACKER.H--Shared Server Class Factory Support Added to the Class Definition ■ Listing 3.32 TRACKER.CPP--Shared Server Update to Class Implementation ■ Listing 3.33 TRACKER.CPP--Shared Server Release Implementation From Here... ■
❍
❍
Creating ActiveX Automation Servers Using MFC ●
●
●
●
●
●
Methods and properties MFC Class Wizard will greatly reduce the time required to implement your server. OLE exceptions MFC uses the class COleDispatchException for generating errors. Dual-interface The user of the Automatic Server users can choose between an IDispatch or COM interface when accessing the server. Dual-interface is not supported directly by MFC. Dual-interface OLE exceptions In dual-interface support, OLE exceptions are generated differently from standard MFC COleDispatchExceptions. Creating servers using C++ C++ can be used to launch servers from the application they are defined in. Shared and single instance servers Accessing an already running server may be required. With a single instance server, the server implement is responsible for the reuse of an already running server.
MFC and Visual C++ (VC++) provide a very simple and easy to use framework for creating ActiveX Automation Servers. In fact, the VC++ development environment's AppWizard and ClassWizard are implemented with this in mind. Creating and manipulating automation interfaces is one of VC++'s primary functions. In this chapter, you will create a simple in-process Automation Server using MFC for logging string data to a file. Throughout this chapter, you can use an application such as Visual Basic (VB) to test your implementation. VB is perfect for accessing Automation Servers since it takes so little time and code to do so. As you proceed through the chapter, you will expand on your implementation, highlighting some of the more advanced concepts of Automation Server creation.
Creating the Basic Project When creating an Automation Server, the first step is to create a basic project upon which you will build your application's features and functionality. MFC provides an AppWizard that greatly simplifies this process. The AppWizard consists of a set of structured dialogs and choices that, in the end, will result in a set of files representing the basic application's project. To create the basic project, you need to open the VC++ Integrated Development Environment (IDE) and from the File menu, select the New menu item. Select the Projects tab in the New dialog, and select MFC AppWizard (dll) as the type. Enter the Project name MFCServer, and set the Location to the \Que\ActiveX\MFCServer directory (see fig. 3.1). Click OK to continue. FIG. 3.1 Select the application type, name, and location of your new project. In the MFC AppWizard -- Step 1 of 1 dialog, you define the specifics about how your application is going to be created (see fig. 3.2). For the type of DLL to create, select Regular DLL with MFC statically linked, which results in a slightly larger application but one that should load faster because you won't have to load the MFC DLLs whenever the server is launched. Also, check the Automation check box--since that is the reason you are creating the application in the first place. Click the Finish button to continue. FIG. 3.2 Define the specific application features in the MFC AppWizard -- Step 1 of 1 dialog. The New Project Information dialog allows you to review your choices before creating the actual project (see fig. 3.3). Click OK to complete the creation of your project. FIG. 3.3 Confirm the project settings in the New Project Information dialog. The MFC AppWizard will create all the basic files that are needed to create a DLL-based Automation Server. Table 3.1 lists all of the files that are created for you and a brief explanation of what they are used for. Table 3.1 Basic Source Files Created by the MFC AppWizard Filename
Description
MFCServer.clw
VC++ project file.
MFCServer.cpp
Main application source file and entry point for the DLL.
MFCServer.def
Standard application DEF file. This file contains the function export declarations needed for all in-process servers.
MFCServer.dsp
VC++ project file.
MFCServer.dsw
VC++ project file.
MFCServer.h
Main application header file.
MFCServer.ncb
VC++ project file.
MFCServer.odl
Standard Object Definition Language (ODL) file.
MFCServer.rc
Standard resource file.
ReadMe.txt
Text file that describes the project.
Resource.h
Resource header file.
StdAfx.cpp
Standard precompiled header source file.
StdAfx.h
Standard precompiled header file. All the MFC-specific include files are added here.
Res\MFCServer.rc2 Standard resource 2 file. This file contains all of the resource information that cannot be edited directly by VC++. At this point, you can compile your project, but you can do very little with it since it does not contain interfaces, methods, or properties.
Adding an Automation Interface to the Application To be an Automation Server, an application must contain at least one or more IDispatch-based interfaces. In MFC, the CCmdTarget class is used to implement this interface. You will use the MFCClassWizard to add your automation interfaces to your application. From the View menu, select the ClassWizard menu item. Click the Add Class button, and select the New menu item to open the New Class dialog (see fig. 3.4). FIG. 3.4 Add a new Automation Server class using the Add Class feature of the MFC ClassWizard. Enter the Name CTracker, and select CCmdTarget as its base class in the Base class combo box. Select the Automation radio button in the Automation radio button group. The Createable by type ID radio button and edit field are used to define the ProgID that will be used to create and launch the Automation Server. The human readable ProgID is used in place of the CLSID since it is much easier to write and remember. Be careful when defining a ProgID not to create duplicates. For your application, leave the ProgID set to its default value. Click OK to create your new class and add it to your application. Click OK in the MFC ClassWizard dialog to close the ClassWizard. When creating a new CCmdTarget class, MFC not only creates a header and source file with all of the appropriate information, Tracker.h and Tracker.cpp in your case, but since you selected automation support, it also updates the ODL file with the new Dispinterface and CoClass entries (see Listing 3.1) of your Automation Server. The Dispinterface is your primary IDispatch-based interface and is where the ClassWizard will add your new methods and properties. The CoClass interface identifies your class factory interface. The class factory is the part of the application that performs the actual creation of your Automation Server when it is necessary to do so. See the OLE and MFC documentation for more information on class factories and their role in OLE.
Listing 3.1 MFCSERVER.ODL--Dispinterface and CoClass ODL Entries // MFCServer.odl : type library source for MFCServer.dll // This file will be processed by the Make Type Library (mktyplib) tool to // produce the type library (MFCServer.tlb). [ uuid(11C82943-4EDD-11D0-BED8-00400538977D), version(1.0) ] library MFCServer { importlib("stdole32.tlb"); // Primary dispatch interface for CTracker [ uuid(11C82946-4EDD-11D0-BED8-00400538977D) ] dispinterface ITracker {
properties: // NOTE - ClassWizard will maintain property information here. // Use extreme caution when editing this section. //{{AFX_ODL_PROP(CTracker) //}}AFX_ODL_PROP methods: // NOTE - ClassWizard will maintain method information here. // Use extreme caution when editing this section. //{{AFX_ODL_METHOD(CTracker) //}}AFX_ODL_METHOD }; // Class information for CTracker [ uuid(11C82947-4EDD-11D0-BED8-00400538977D) ] coclass TRACKER { [default] dispinterface ITracker; }; //{{AFX_APPEND_ODL}} }; Even though the MFC ClassWizard added the new interface to the server, it did not expose the interface to the outside world. Basically, what you have right now is an Automation Server that cannot be created by any application. Not very useful. All ActiveX components are created through an object known as a class factory. MFC defines the class COleObjectFactory for its class factory support. You do not add the COleObjectFactory class directly to your server implementation; instead, you need to use two macros defined by MFC: DECLARE_OLECREATE and IMPLEMENT_OLECREATE. In the Project Workspace, select the ClassView tab. Expand the class list, and double-click the CTracker class to open the Tracker.h file. Add the macro DECLARE_OLECREATE to your class definition as in Listing 3.2. The macro takes a single parameter, CTracker, which is your class name.
Listing 3.2 TRACKER.H--Add the Class Factory Support with the Macro DECLARE_OLECREATE . . . // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_DISPATCH DECLARE_DISPATCH_MAP() DECLARE_INTERFACE_MAP() DECLARE_OLECREATE(CTracker) }; Next in the Project Workspace window, select the FileView tab, expand the Source Files list, and double-click the Tracker.cpp entry to open the file. Add the macro IMPLEMENT_OLECREATE to your source file (see Listing 3.3). The IMPLEMENT_OLECREATE macro takes three parameters: the class name, the ProgID that is used to create the server, and the CLSID of the CoClass interface, as defined in your ODL file. When the ODL file was created by the AppWizard, a CLSID was generated for the type library. When the ClassWizard added the CTracker class, it too created new CLSIDs; one for the Dispinterface and the other for the CoClass.
Listing 3.3 TRACKER.CPP--Add the Class Factory Implementationwith the IMPLEMENT_OLECREATE Macro . . . // {11C82946-4EDD-11D0-BED8-00400538977D} static const IID IID_ITracker = { 0x11c82946, 0x4edd, 0x11d0, { 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d } }; BEGIN_INTERFACE_MAP(CTracker, CCmdTarget) INTERFACE_PART(CTracker, IID_ITracker, Dispatch) END_INTERFACE_MAP() IMPLEMENT_OLECREATE(CTracker, _T("MFCServer.Tracker"), 0x11C82947, 0x4edd, 0x11d0, 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d) . . . Your server implementation is now class factory enabled, which allows it to be created by other applications. Before another application can create the server, however, OLE has to know where to find the server, which is done through the system registry. All ActiveX components that are publicly available to other applications must support registration and must create valid registry entries.
Registry ActiveX components have one or more registry entries that are used to describe various aspects of the application and how it can be used. The registry is critical to the successful launching and using of ActiveX components.
Server Registration Local servers rely on command-line options for registration support. It is the responsibility of the local server developer to check for the correct command-line option and take the appropriate action. Table 3.2 Local Server Command-Line Options for Registration Support Command-Line Option Description R
Register all components.
U
Unregister all components.
S
Perform registration in silent mode and do not display confirmation dialogs, indicating success. Error messages should still be displayed. This option can be combined with R or U.
All inproc ActiveX components expose registration support via two exported functions: DllRegisterServer and DllUnregisterServer. The MFC AppWizard will automatically add the DllRegisterServer function to the main application file of a project when it is created. The registration of all of the components contained in the application should be performed in this function, with each ActiveX component being responsible for its own registration support. Registration support is handled automatically by the COleObjectFactory class. Even though you may not be aware of it, the COleObjectFactory class contains a singly linked list that is used to keep track of all of the
COleObjectFactory classes implemented in a single application. The linked list is a static member, which means that all instances of the class share the same class factory list. COleObjectFactory also contains a static function, UpdateRegistryAll, that will cycle through the list of COleObjectFactory classes, instructing each to register themselves.
Server Unregistration The MFC AppWizard does not add the exported function, DllUnregisterServer, to a project when it is created. This is probably due to an inherent limitation in MFC. The MFC group apparently did not feel it was necessary to add unregistration support to the basic MFC COleObjectFactory class. This is very interesting since all of the Microsoft logo requirements indicate that all applications that are installed and registered must also uninstall and unregister themselves in order to qualify for the logo. To support server unregistration, you would have to add the exported function, DllUnregisterServer and call the static function COleObjectFactory::UpdateRegistryAll passing FALSE as the parameter. The actual unregistration code requires more work. We didn't include the unregistration code as a part of the sample code, but the implementation is straightforward and is outlined here. The first step is to create a new class that inherits from COleObjectFactory, and override the virtual function, UpdateRegistry. Check the parameter that is passed to the function, and based on its value, call the appropriate registration and unregistration code. MFC provides a basic registration helper function, AfxOleRegisterServerClass, but does not define a companion helper function for unregistration. Searching the source files in MFC reveals a complete set of helper functions for the registry, but unfortunately they are not accessible from anything but an MFC-implemented ActiveX control. Since nothing is available from MFC, you are required to implement the registry updating code yourself. Remember to remove all of the registry entries that the server created: the ProgID, the CLSID, and the type library.
Sample Server Support Code Since your sample server is used to output data to a file, you first need to add some support code to your application before adding its methods and properties. Listing 3.4 shows the changes and additions that have been made to the class header file. A set of member variables was added for storing the file handle and timer information that will be used throughout the server implementation.
Listing 3.4 TRACKER.H-- Sample Server Support Code Added to the Header File . . . DECLARE_OLECREATE(CTracker) protected: FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime; }; The next step is to update the source file for the class. Add the include file mmsystem.h to the Tracker.cpp file (see
Listing 3.5). This is for the timer functions that you are taking advantage of in the sample server implementation. The constructor and destructor of the server have also been updated. When using OLE in MFC applications, you must always lock the application in memory by calling the method AfxOleLockApp(), which ensures that the application will not be unloaded from memory until the reference count reaches zero. This step is critical and must be in all MFC-based servers. Next you create a high resolution timer and store its current value in your member variables. The timer is useful for determining the number of milliseconds that have passed since the last method call was made. The timer output is great for tracking the performance of a particular action or set of actions. You then get the current date and create a filename with the format YYYYMMDD.tracklog. After successfully opening the file, you output some start-up data to the file and exit the constructor. The destructor does the exact opposite of the constructor. If there is a valid file handle, you write some closing information to the file and close it. Next you terminate the timer. Remember to call the function AfxOleUnlockApp() to allow the application to be removed from memory.
Listing 3.5 TRACKER.CPP--Updated Source File . . . #include "Tracker.h" // needed for the high resolution timer services #include #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CTracker IMPLEMENT_DYNCREATE(CTracker, CCmdTarget) CTracker::CTracker() { EnableAutomation(); // make sure that the application won't unload until the reference count is ::AfxOleLockApp(); // setup our timer resolution m_lTimeBegin = timeBeginPeriod(1); m_lHiResTime = m_lLastHiResTime = timeGetTime(); // get the current date and time CTime oTimeStamp = CTime::GetCurrentTime(); CString cstrFileName; // create a file name based on the date cstrFileName.Format(_T("%s.tracklog"), (LPCTSTR) oTimeStamp.Format("%Y%m%d")); // open a file m_fileLog = fopen(cstrFileName, _T("a")); // if we have a file handle if(m_fileLog) {
// output some starting information fprintf(m_fileLog, _T("************************\n")); fprintf(m_fileLog, _T("Start %s\n"), (LPCTSTR) oTimeStamp.Format ("%B %#d, %Y, %I:%M %p")); fprintf(m_fileLog, _T("\n")); } } CTracker::~CTracker() { // if we have a file handle if(m_fileLog) { // output some closing information CTime oTimeStamp = CTime::GetCurrentTime(); fprintf(m_fileLog, _T("\n")); fprintf(m_fileLog, _T("End %s\n"), oTimeStamp.Format ("%B %#d, %Y, %I:%M %p")); fprintf(m_fileLog, _T("************************\n")); // close the file fclose(m_fileLog); } // if we have valid timer services if(m_lTimeBegin == TIMERR_NOERROR) // reset the timer to its original state timeEndPeriod(1); // make sure that the application can unloaded ::AfxOleUnlockApp(); } . . . Finally you update the build settings for the project. Since the sample implementation is using some timer functions defined in mmsystem.h, you also need to be linked with the appropriate library file that contains their implementation. Under the Project menu, select the Settings menu item. In the Project Settings dialog, from the Settings For drop-down list box, select the All Configurations entry. Select the Link tab, and add the file winmm.lib to the Object/library modules edit field. Click OK to close the dialog. The basic support code needed for the sample implementation is now added. The server will open a file in its constructor and leave the file open during its entire lifetime. When the server is destroyed, the destructor will be called, and the file will be closed. The next step is to make the sample more meaningful by adding methods and properties, which are used to output data to the open file.
Adding Methods An automation method consists of zero to n parameters and may or may not have a return value. The term method is synonymous with function or subroutine, depending on the particular language you are familiar with. Since your server is IDispatch-based, you are limited to a specific set of data types. Only those data types that are valid VARIANT data types can be passed or returned via a method.
The rules for declaring parameters and how they are used is very much like C++ and VB. Methods can pass parameters by value or by reference and may also declare them as optional, meaning that the parameter does not have to be supplied. When passing a parameter by value, a copy of the data is sent to the method, and when passing by reference, the address of the parameter is passed, allowing the method to change the data. Optional parameters are handled a little differently than C++, however, since you can't specify a default value in the traditional C++ sense. Optional parameters must be passed as VARIANT data types and not the actual data type they represent. For developers using VB to access a method with optional parameters, VB will supply the parameter for you if one has not been provided. With C++, you are still required to supply a VARIANT parameter even though it may not contain any data. As we stated at the beginning of the chapter, the sample Automation Server will be used to log strings of data to a file. The server will define the method OutputLines, which is used by the user of the server to supply the string data that is written to the file. The method will accept an array of strings and an optional indentation parameter and will output the strings to the file. The indentation parameter is used to offset the strings by n number of tab characters to provide simple, yet effective, formatting to the data as it is output to the file. From the View menu, select the ClassWizard menu item. Select the Automation tab, and click the Add Method button. In the Add Method dialog enter an External name of OutputLines and a Return type of BOOL (see fig. 3.5). FIG. 3.5 Add the OutputLines method with the ClassWizard.
Boolean Data Type Differences Between VC++ and VB It is important to note a fundamental difference between VC++ and VB when using Boolean data types. The Boolean data type is defined by C++ as being of type integer that is a 32-bit value. For VB, however, an integer is 16-bit. For simple MFC-based Automation Servers, the difference in sizes between a VB integer and VC++ integer is not a problem since MFC hides the details involving the conversion of the 32-bit value to a 16-bit value, and vice versa. For dual-interface applications, though, the size difference poses a significant problem. When accessing the custom interface of a dual-interface server, the functions are called in the same fashion as any other function in an application. Basically, the parameters of the function are pushed on to a stack, and the function is called. When the function executes, the parameters are then popped off the stack. If VB calls a function in VC++, the stack will become corrupt because of the different sizes that each language uses for the Boolean data type. To be safe, VC++ applications should define all Boolean data types as type VARIANT_BOOL, which is defined by OLE as a 16-bit value and which is guaranteed to be the same size regardless of the language or tool being used. The actual Boolean data value is different between VB and VC++ also. VB developers are used to Boolean values of 0 indicating FALSE and -1 indicating TRUE. For those of you who may be wondering why, the binary values for each is 00000000 and 11111111, respectively. For C++ programmers, Boolean data values are usually defined as 0 for FALSE and 1 or non-zero for TRUE.
The differences in Boolean data values can cause considerable problems when integrating VB and VC++
applications. In addition, VB 4 has some behavioral differences in its language, depending on the value being tested. Some VB functions do not test for 0 or non-zero and will test for the absolute value of 0 or -1, and vice versa, depending on the data type and function. When using Boolean data types, it is wise to also use the VARIANT_FALSE and VARIANT_TRUE constants to define the value of the variable. OutputLines is defined as having two parameters: varOutputArray as a VARIANT passed by reference, which will contain a string array of data to output to the file, and varIndent as a VARIANT passed by value, which is also an optional parameter indicating the amount of indentation when writing the string data to the file. To add the method parameters, double-click the line in the Parameter list that is directly below the Name column, and type varOutputArray. Click directly under the Type column to activate the Type drop-down list box. Select VARIANT * from the list. Repeat the same process for varIndent, but set the data type to VARIANT. Due to data type restrictions imposed by Automation, you cannot pass arrays as parameters of methods. You can, however, pass VARIANT data types that can contain arrays, thus the reason for defining varOutputArray as a VARIANT. You are also required to pass varOutputArray by reference because the array stored in the VARIANT does not get copied over when it is passed by value. Optional parameters must fall at the end of the parameter list and must be of type VARIANT (see Listing 3.6). varIndent is an optional parameter that indents the text output as an added formatting feature. Click OK to add the method. Click OK in the MFC ClassWizard dialog to close the ClassWizard. Remember that the ClassWizard also added an entry to the ODL file as well as the header and source files. It is a function of the ODL file to declare a parameter of a method as being optional. To be optional, a parameter must be declared with the optional parameter attribute (see Listing 3.6), which you are required to add by hand since the ClassWizard will not add it for you.
Listing 3.6 MFCSERVER.ODL--Updated ODL Entry for OutputLines Method // NOTE - ClassWizard will maintain method information here. // Use extreme caution when editing this section. //{{AFX_ODL_METHOD(CTracker) [id(1)] boolean OutputLines(VARIANT* varOutputArray, [optional] VARIANT varIndent); //}}AFX_ODL_METHOD Before you add the implementation of the OutputLines method, you need to add a member variable to the class definition (see Listing 3.7). The new member, m_lIndent, is used to store the current indentation level between calls to the method OutputLines.
Listing 3.7 TRACKER.H--New Member Variable Added to the Tracker Class protected: FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime;
long m_lIndent; }; You also need to update the constructor to initialize the member to a valid starting value (see Listing 3.8).
Listing 3.8 TRACKER.CPP--Member Initialization in the Constructor CTracker::CTracker() { . . . m_lIndent = 0; } Listing 3.9 shows the implementation of the OutputLines method. First you check to see if you have a valid file handle and array of string data. The next step is to lock down the array so that you can perform operations on it. This step is required for all functions that manipulate safe arrays. The next function determines the starting point of the array, which can be either 0 or 1. This procedure is very important to implement since programming languages such as C++ define a base of 0 for arrays, and languages such as VB can define a base of 0 or 1. Next you retrieve the number of dimensions in the array. Note that this value represents the number of dimensions and not the last dimension relative to the lower bound value. After establishing the boundaries of the array, you check to see if you have received an indentation value also. You want to receive a long, VT_I4, but if you don't receive it, you try to convert the data that was given to you to a usable value. If you can't convert the data, you simply use the value that the variable already contains. To indent the text, concatenate from 1 to n tab characters into a string. For each of the elements in the array of strings, get the current time and the data associated with each element, and output them along with the indentation string to the open file--and don't forget to free the string element when you finish with it. The last step is to unlock the array and exit the method with the proper return value.
Listing 3.9 TRACKER.CPP--OutputLines Method Implementation . . . ///////////////////////////////////////////////////////////////////////////// // CTracker message handlers BOOL CTracker::OutputLines(VARIANT FAR* varOutputArray, const VARIANT FAR& varIndent) { BOOL bResult = VARIANT_TRUE; // if we have a file a if the variant contains a string array if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR)) { // lock the array so we can use it if(::SafeArrayLock(varOutputArray->parray) == S_OK) { LONG lLBound; // get the lower bound of the array if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK)
{ LONG lUBound; // get the number of elements in the array if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound) == S_OK) { CString cstrIndent; CTime oTimeStamp; BSTR bstrTemp; // if we have an indent parameter if(varIndent.vt != VT_I4) { // get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType(&varConvertedValue, (VARIANT *) &varIndent, 0, VT_I4)) // assign the value to our member variable m_lIndent = varConvertedValue.lVal; } else // assign the value to our member variable m_lIndent = varIndent.lVal; // if we have to indent the text for(long lIndentCount = 0; lIndentCount < m_lIndent; lIndentCount++) // add a tab to the string cstrIndent += _T("\t"); // for each of the elements in the array for(long lArrayCount = lLBound; lArrayCount < (lUBound + lLBound); lArrayCount++) { // update the time oTimeStamp = CTime::GetCurrentTime(); m_lHiResTime = timeGetTime(); // get the data from the array if(::SafeArrayGetElement(varOutputArray->parray, &lArrayCount, &bstrTemp) == S_OK) { // output the data fprintf(m_fileLog, _T("%s(%10ld)-%s%ls\n"), (LPCTSTR) oTimeStamp.Format("%H:%M:%S"), m_lHiResTime - m_lLastHiResTime, (LPCTSTR) cstrIndent, bstrTemp); // store the last timer value m_lLastHiResTime = m_lHiResTime; // free the bstr ::SysFreeString(bstrTemp); } } } else
bResult = VARIANT_FALSE; } else bResult = VARIANT_FALSE; // unlock the array we don't need it anymore ::SafeArrayUnlock(varOutputArray->parray); } else bResult = VARIANT_FALSE; } else bResult = VARIANT_FALSE; // return the result return bResult; } It is worthwhile to review the documentation in the VC++ books online on ODL, Automation, and VARIANT data types to see what kind of flexibility you have when defining methods and parameters. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating methods. Now that you have added a method, you are ready to implement its counterpart, the property.
Adding Properties A property can be thought of as an exposed variable that is defined in the Automation Server. Properties are useful for setting and retrieving information about the state of the server. Properties are implemented as a pair of methods: one to get the value, and the other to set the value. The m_lIndent member variable that you added to the class definition is a perfect candidate to be exposed as a property. As with methods, properties are also added via the ClassWizard in MFC. From the View menu, select the ClassWizard menu item. In the MFC ClassWizard dialog, select the Automation tab, and click the Add Property button. In the Add Property dialog, enter the External name of the property as Indent and select the type as long (see fig. 3.6). Set the Implementation to Get/Set methods, and click OK to add the property to the server. Click the Edit Code button to close the MFC ClassWizard dialog and open the source file for editing. FIG. 3.6 Add the Indent property with the ClassWizard. The actual implementation of the Indent property is very easy (see Listing 3.10). GetIndent returns the value currently stored in the member variable, and SetIndent stores the new value, after a little bit of error checking, in the member variable.
Listing 3.10 TRACKER.CPP--Indent Property Implementation long CTracker::GetIndent() { // return the member variable
return m_lIndent; } void CTracker::SetIndent(long nNewValue) { // if the new value is a least 0 if(nNewValue >= 0) // assign the value to our member variable m_lIndent = nNewValue; } Properties, like methods, also have a wide variety of implementation options, including parameterized and enumerated values. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating properties. You've added methods and properties to the server but haven't really dealt with the issue of error handling in their implementation. In some cases, simply returning success or failure is not enough information for the developer to understand that an error occurred and what caused it. You will communicate more error information through the use of OLE exceptions.
Generating OLE Exceptions While executing a method call or some other action, at times it is necessary to terminate the process due to some critical error that has occurred or is about to occur. For example, a method is called to write data to a file, but the method cannot open the file because there is not enough room on the hard disk to do so. It is necessary to halt further processing until the error can be resolved. An error of this kind is known as an exception. Any type of error can be treated as an exception; it depends on the requirements of your application and how you choose to deal with the errors that may result. You must become familiar with two forms of exceptions when creating ActiveX components. The first is a C++ exception. A C++ exception is a language mechanism used to create critical errors of the type described earlier and is confined to the application in which they are defined. The second is an OLE exception. OLE exceptions are used to communicate the same kinds of errors externally to applications that are using a component. The difference between the two is that C++ exceptions are used internal to an application's implementation, and OLE exceptions are used externally to communicate errors to other applications. The IDispatch interface contains specific parameters in its functions for dealing with exceptions and passing them to the controller application. The MFC implementation of the CCmdTarget class handles the details of generating OLE exceptions by trapping any C++ exceptions that it receives and translates them to the proper IDispatch error information. You need only create a C++ exception of type COleDispatchException and throw it. MFC does all of the work for you. When creating dual-interface servers, exceptions are handled in a different way, which you will see later in this chapter. The first step is to add an enumeration of the types of errors that the server can generate to the ODL file (see Listing 3.11). Adding the enumeration to the ODL has the effect of publishing the error constants to the applications developer that is using the server. You add the constants in the form of an include file so that you can use the same error constants file in the C++ source code implementation. You also add a UUID to the typedef so that it can be identified in the type library that is generated with the GUIDGEN.EXE program.
Listing 3.11 MFCSERVER.ODL--Error Enumeration
. . . [ uuid(11C82947-4EDD-11D0-BED8-00400538977D) ] coclass TRACKER { [default] dispinterface ITracker; }; typedef [uuid(11C82948-4EDD-11D0-BED8-00400538977D), helpstring("Tracker Error Constants")] #include "trackererror.h" //{{AFX_APPEND_ODL}} }; TrackerError.h contains a standard C/C++ enumeration of the errors that the application supports (see Listing 3.12). The starting value of the errors falls into the range of valid user-defined errors. Be careful when assigning error numbers since most tools will first look up the system-defined error message before using the message defined in the exception.
Listing 3.12 TRACKERERROR.H--Tracker Error Constants // Error enumeration enum tagTrackerError { MFCSERVER_E_NO_UBOUND = 46080, MFCSERVER_E_NO_LBOUND = 46081, MFCSERVER_E_NO_ARRAYLOCK = 46082, MFCSERVER_E_NO_FILE = 46083, MFCSERVER_E_BAD_ARRAY_PARAMETER = 46084, MFCSERVER_E_INVALID_VALUE = 46085 }TRACKERERROR; The next step is to add the code that will generate the exceptions to all of the appropriate locations in the server code (see Listing 3.13). As you can see, instead of returning VARIANT_FALSE or ignoring an error condition, you now generate meaningful errors and messages instructing the developer as to the source of the problem. The exception-generating code is fairly straightforward. First you create a COleDispacthException and set the appropriate members with the data that is necessary for the error that was generated (for information about other types of exceptions, see the VC++ documentation). For your implementation, you set the error code, the name of the file that generated the error, and the error message. You could also supply a help filename and a help ID to further describe the error. Note the use of the MAKE_SCODE macro to generate a valid SCODE error number for the exception.
Listing 3.13 TRACKER.CPP--Exception Handling Code Added to the Source Files ///////////////////////////////////////////////////////////////////////////// // CTracker message handlers BOOL CTracker::OutputLines(VARIANT FAR* varOutputArray, const VARIANT FAR& varIndent) { BOOL bResult = VARIANT_TRUE;
// if we have a file a if the variant contains a string array if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR)) { // lock the array so we can use it if(::SafeArrayLock(varOutputArray->parray) == S_OK) { LONG lLBound; // get the lower bound of the array if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK) { LONG lUBound; // get the number of elements in the array if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound) == S_OK) { CString cstrIndent; CTime oTimeStamp; BSTR bstrTemp; // if we have an indent parameter if(varIndent.vt != VT_I4) { // get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType(&varConvertedValue, (VARIANT *) &varIndent, 0, VT_I4)) // assign the value to our member variable m_lIndent = varConvertedValue.lVal; } else // assign the value to our member variable m_lIndent = varIndent.lVal; // if we have to indent the text for(long lIndentCount = 0; lIndentCount < m_lIndent; lIndentCount++) // add a tab to the string cstrIndent += _T("\t"); // for each of the elements in the array for(long lArrayCount = lLBound; lArrayCount < (lUBound + lLBound); lArrayCount++) { // update the time oTimeStamp = CTime::GetCurrentTime(); m_lHiResTime = timeGetTime(); // get the data from the array if(::SafeArrayGetElement(varOutputArray->parray, &lArrayCount, &bstrTemp) == S_OK) { // output the data fprintf(m_fileLog, _T("%s(%10ld)-%s%ls\n"), (LPCTSTR) oTimeStamp.Format("%H:%M:%S"), m_lHiResTime - m_lLastHiResTime, (LPCTSTR)
cstrIndent, bstrTemp); // store the last timer value m_lLastHiResTime = m_lHiResTime; // free the bstr ::SysFreeString(bstrTemp); } } } else { bResult = VARIANT_FALSE; // unable to get a record based on the sql statement - throw an exception COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0); // format the error code pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_UBOUND); // set the source file pOleDispExcep->m_strSource = __FILE__; // format the error description pOleDispExcep->m_strDescription = _T("Unable to retrieve the upper bound dimension of the array."); // the function call failed cause an ole exception throw(pOleDispExcep); } } else { bResult = VARIANT_FALSE; // unable to get a record based on the sql statement - throw an exception COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0); // format the error code pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_LBOUND); // set the source file pOleDispExcep->m_strSource = __FILE__; // format the error description pOleDispExcep->m_strDescription = _T("Unable to retrieve the lower bound dimension of the array."); // the function call failed cause an ole exception throw(pOleDispExcep); } // unlock the array we don't need it anymore ::SafeArrayUnlock(varOutputArray->parray); } else { bResult = VARIANT_FALSE; // unable to get a record based on the sql statement - throw an exception COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0);
// format the error code pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_ARRAYLOCK); // set the source file pOleDispExcep->m_strSource = __FILE__; // format the error description pOleDispExcep->m_strDescription = _T("Unable to lock the array memory."); // the function call failed cause an ole exception throw(pOleDispExcep); } } else { bResult = VARIANT_FALSE; // unable to get a record based on the sql statement - throw an exception COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0); // if we didn't have a file handle if(!m_fileLog) // format the error code pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE); else // format the error code pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_BAD_ARRAY_PARAMETER); // set the source file pOleDispExcep->m_strSource = __FILE__; // if we didn't have a file handle if(!m_fileLog) // format the error description pOleDispExcep->m_strDescription = _T("Invalid File Handle. File could not be opened for output."); else // format the error description pOleDispExcep->m_strDescription = _T("The first parameter must be a string array passed by reference."); // the function call failed cause an ole exception throw(pOleDispExcep); } // return the result return bResult; } long CTracker::GetIndent() { // return the member variable return m_lIndent; } void CTracker::SetIndent(long nNewValue) { // if the new value is a least 0 if(nNewValue >= 0)
// assign the value to our member variable m_lIndent = nNewValue; else { // unable to get a record based on the sql statement - throw an exception COleDispatchException * pOleDispExcep = new
COleDispatchException(_T(""), NULL, 0); // format the error code pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_INVALID_VALUE); // set the source file pOleDispExcep->m_strSource = __FILE__; // format the error description pOleDispExcep->m_strDescription = _T("Invalid value. Value must be 0 or greater."); // the function call failed cause an ole exception throw(pOleDispExcep); } } Exceptions are useful for communicating error conditions and problems back to the application and developer who are using an ActiveX component. Make use of them whenever you can to further enhance your implementation.
Dual-Interface Dual-interface is exactly what it sounds like: The server implementation supports two interfaces with which to talk to the server. One interface, an IDispatch interface, is what you have been working with so far. The other, a custom interface, is a type of interface that you have not looked at yet. The dual portion refers to the fact that no matter which interface you choose, you are always talking to the same server, and you will always get the same response. An IDispatch-based interface uses a generic mechanism for calling methods and properties in a server. When a method in a server is called, you pass the ID of the method to invoke and a structure describing its parameters and return type. This data is packaged and sent to the server, which unpackages the data and calls the appropriate method based on the ID supplied. A custom interface, on the other hand, is very different. When using a custom interface, you are talking directly to the server's functions and are not depending on a generic mechanism for invoking the methods or properties. The packaging of the parameters and return value are left to the compiler that created the applications. Since the interfaces are written to access the same set of functions, the custom interface portion of a dual-interface server must conform to the same data type restrictions imposed by Automation. This way, you are not required to create your own code to transfer data between the two applications: the controller and the server. OLE does that for you with standard marshaling. The major advantage to dual-interface is performance. The number of steps to call a method using the custom interface is far less than the number needed to call a method when using the IDispatch interface.
The main disadvantage to dual-interface support in MFC servers is that they are not supported by the ClassWizard and will require manual changes to implement. The code involved is not too difficult to maintain; the hardest part is probably remembering to do it.
NOTE: The advantage of using the custom interface of a dual-interface server loses its luster when executing across process boundaries. The custom interface is still faster, but not by much. The real performance benefit, a 25 to 50 percent improvement, is when the server is in-process to the calling application. The amount of improvement depends on the number and types of parameters that are being passed between the applications. If you are interested in seeing actual numbers regarding the amount of performance improvement, you can refer to recent Microsoft Systems Journal articles, which have focused on performance differences between IDispatch and custom interfaces.
The first step when converting an MFC-based ActiveX server to dual-interface is to change the ODL file. Listing 3.14 shows the changes that have been made to the server to support dual-interface. It is not necessary to generate new UUIDs because the functionality of the server has not changed. You must add the oleautomation and dual attributes to the interface class, though. You also add the hidden attribute, so this interface will not be visible within VB. This is fine since VB will display the CoClass interface and will also show all of the methods and properties for the server. As a general rule, you should always hide your interfaces and leave visible the CoClasses used to create them. This is because applications like VB will display both interfaces, and the reality is that only the CoClass name is valid in VB; if you try to reference an object by its interface name, you will get an error. Since the server supports dual-interface, you must change your interface declaration to inherit from the IDispatch interface rather than declare the interface as type dispinterface, as in the original implementation. Dual-interface method and property declarations are different from dispinterface declarations, which are more like standard C++. Note that keywords such as method and properties are no longer within the interface declaration. Those terms are keywords related to the dispinterface keyword. As we stated earlier, properties are accessed using a pair of methods sharing the same dispid. The distinguishing factors are the method attributes propget and propput, which denote the direction of data flow. You must also change the CoClass to refer to interface and not dispinterface. All dual-interface methods must return an HRESULT data type. If a method requires a return value, it must be specified as the last parameter of the method and must have the parameter attributes of out and retval. All parameters must have an attribute describing the direction of data flow. See Table 3.3 for a complete description of the possible attributes and combinations. Table 3.3 Parameter Flow Attributes Direction
Description
in
Parameter is passed from caller to callee.
out
Parameter is returned from callee to caller.
in, out
Parameter is passed from caller to callee, and the callee returns a parameter.
out, retval
Parameter is the return value of the method and is returned from the callee to the caller.
Listing 3.14 MFCSERVER.ODL--ODL Changes to Support Dual-Interface [ uuid(11C82943-4EDD-11D0-BED8-00400538977D), version(1.0) ]
library MFCServer { importlib("stdole32.tlb"); [ uuid(11C82946-4EDD-11D0-BED8-00400538977D), oleautomation, dual, hidden ] interface ITracker: IDispatch { [id(1), propget] HRESULT Indent([out, retval] long * Value); [id(1), propput] HRESULT Indent([in] long Value); [id(2)] HRESULT OutputLines([in] VARIANT * varOutputArray, [in, optional] VARIANT varIndent, [out, retval] boolean * RetVal); }; // CoClass for CTracker [ uuid(11C82947-4EDD-11D0-BED8-00400538977D) ] coclass TRACKER { [default] interface ITracker; }; typedef [uuid(11C82948-4EDD-11D0-BED8-00400538977D), helpstring("Tracker Error Constants")] #include "trackererror.h" //{{AFX_APPEND_ODL}} }; The remainder of the ODL file entries do not have to be changed to support dual-interface.
NOTE: The changes that have been made to the ODL file will prevent the ClassWizard from updating the ODL file automatically when new methods and properties are added. You are now responsible for maintaining the entries manually.
The ODL compiler has the capability of generating a C++ header file that describes all of the interfaces and enumerations that are contained in the type library that you've created for your server. The ODL-generated header file is useful for creating function prototypes that are required in the implementation of the server. You add the entry to the ODL file and compile it into a type library. Next copy the new method from the header file into your class definition and make some minor changes--and everything is finished. In addition, you now have an interface file that can be used by other applications to access the custom interface of your server as well as the enumerations that are used when accessing specific methods and properties. To generate the header file, you must update the build settings of your project. From the Project menu, select the Settings menu item. In the Settings For drop-down list box found in the Project Settings dialog, select All Configurations (see fig. 3.7). Expand the MFCServer project node and the Source Files node, and select the MFCServer.odl file. Select the OLE Types tab, and in the Output header file name edit field, enter the name TrackerInterface.h. Whenever the type library is compiled, the TrackerInterface.h will be regenerated to reflect the new implementation. FIG. 3.7 Update the project settings to create the C++ header file from the ODL file.
Listing 3.15 shows the inclusion of the new header file to the Tracker.cpp source file. Note that you also remove the TrackerError.h file because the error enumeration that you declared earlier in the chapter is also defined in the header file generated by the ODL compiler. The TrackerInterface.h must be included before or in the Tracker.h file; the CTracker class is dependent on the information in the TrackerInterface.h file.
Listing 3.15 TRACKER.CPP--ODL-Generated Header File Is Added to the Tracker Source File . . . #include "MFCServer.h" // ODL generated interface file #include "trackerinterface.h" #include "Tracker.h" // needed for the high resolution timer services #include #ifdef _DEBUG #define new DEBUG_NEW . . . MFC defines a set of macros for describing interfaces within the context of an MFC component implementation. The interface macro defines the interface, its name, and the methods that it contains. Listing 3.16 shows the MFC interface declaration that is added to your class definition to describe the custom interface portion of your server. The interface declaration can be added anywhere after the DECLARE_INTERFACE_MAP macro. The first parameter of the BEGIN_INTERFACE_PART macro, SubDispatch, is the name that is used to create a nested class within the class definition of the server. The second parameter is the name of the interface from which the nested class is inherited. The ITracker interface is declared in the TrackerInterface.h header file that was created from the ODL file. The ITracker declaration in the header file contains a set of pure virtual functions that need to be copied into your interface declaration. When copying the functions, remember to remove the = 0 from the end of the function, since this is where you will implement them.
Listing 3.16 TRACKER.H--Interface Macro Update of the Tracker Class Definition . . . // needed for dual interface support BEGIN_INTERFACE_PART(SubDispatch, ITracker) STDMETHOD(GetTypeInfoCount)(THIS_ UINT FAR* pctinfo); STDMETHOD(GetTypeInfo)(THIS_ UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo); STDMETHOD(GetIDsOfNames)(THIS_ REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid, DISPID FAR* rgdispid); STDMETHOD(Invoke)(THIS_ DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr); virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_Indent( /* [retval][out] */ long __RPC_FAR *Value);
virtual /* [propput][id] */ HRESULT STDMETHODCALLTYPE put_Indent( /* [in] */ long Value); virtual /* [id] */ HRESULT STDMETHODCALLTYPE OutputLines( /* [in] */ VARIANT __RPC_FAR *varOutputArray, /* [optional][in] */ VARIANT varIndent, /* [retval][out] */ VARIANT_BOOL __RPC_FAR *RetVal); END_INTERFACE_PART(SubDispatch). . . The header file contains the interface declaration, and the source file contains the interface implementation. The MFC AppWizard created a default interface implementation for the server when it was created. The original definition deferred to the default IDispatch interface defined in MFC. Since you have implemented an interface that inherits from IDispatch, it is necessary to route all IDispatch calls to the new interface. The interface map located in the server source file must be changed to reflect the new interface that you've declared. Change the interface from IDispatch to the name of the interface declared in the header file, in your case SubDispatch, as in Listing 3.17. Changing the name of the interface will have the effect of routing all calls to the IDispatch interface to your implementation of the interface first, which you can then implement yourself or pass on to the default implementation. Unfortunately, MFC does not allow for true C++ inheritance of the COM interfaces it contains, so it is necessary to route messages for a particular COM interface to the correct handler function in the server. Routing the messages is done through the BEGIN_INTERFACE_MAP macro. Since your implementation supports both an IDispatch interface and a custom interface, you are required to add to entries to the macro. In your case, you defer all IDispatch messages to the custom interface functions. The same is true of the custom interface messages.
Listing 3.17 TRACKER.CPP--Interface Implementation of the ITracker Interface . . . BEGIN_INTERFACE_MAP(CTracker, CCmdTarget) INTERFACE_PART(CTracker, IID_IDispatch, SubDispatch) INTERFACE_PART(CTracker, IID_ITracker, SubDispatch) END_INTERFACE_MAP() . . . The last step in adding dual-interface support to your server is to add the actual implementation of the functions declared in the interface. The first set of functions to implement is the base IDispatch functions that your server inherited from the IDispatch class. Listing 3.18 shows the implementation of the functions. In all cases, you defer to the base class implementation of the method. You have the option to implement these functions yourself or rely on MFC to handle the methods for you. An important thing to note is the method used to call the basic IDispatch functions. Since the server captures all IDispatch messages and routes them to the custom interface implementation, you cannot call GetIDispatch() to retrieve the pointer to the IDispatch interface of the server. The reason for this is that it will result in a recursive call since the IDispatch functions are routed to your server implementation; instead, the implementation calls the IDispatch functions ((IDispatch*)&pThis->m_xDispatch)-> directly, bypassing the message routing functions, thus avoiding the recursion problem. You also note that the nested class SubDispatch is declared as XSubDispatch in the implementation. The X comes from the BEGIN_INTERFACE_PART macros and has no particular significance. The same is true for the member variable m_xDispatch.
Note the use of the macro METHOD_PROLOGUE, which is required in MFC-based applications to ensure that MFC is in a valid state while the function is executing.
Listing 3.18 TRACKER.CPP--IDispatch Function Implementation for a DualInterface Server ///////////////////////////////////////////////////////////////////////////// // CTracker Standard IDispatch Dual Interface Handlers ULONG FAR EXPORT CTracker::XSubDispatch::AddRef() { METHOD_PROLOGUE(CTracker, SubDispatch) return pThis->ExternalAddRef(); } ULONG FAR EXPORT CTracker::XSubDispatch::Release() { METHOD_PROLOGUE(CTracker, SubDispatch) return pThis->ExternalRelease(); } HRESULT FAR EXPORT CTracker::XSubDispatch::QueryInterface(REFIID riid, LPVOID FAR* ppvObj) { METHOD_PROLOGUE(CTracker, SubDispatch) return (HRESULT) pThis->ExternalQueryInterface(&riid, ppvObj); } HRESULT FAR EXPORT CTracker::XSubDispatch::GetTypeInfoCount(UINT FAR* pctinfo) { METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)->GetTypeInfoCount(pctinfo); } HRESULT FAR EXPORT CTracker::XSubDispatch::GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo) { METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)->GetTypeInfo(itinfo, lcid, pptinfo); } HRESULT FAR EXPORT CTracker::XSubDispatch::GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid, DISPID FAR* rgdispid) { METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)->GetIDsOfNames(riid, rgszNames, cNames, lcid, rgdispid); } HRESULT FAR EXPORT CTracker::XSubDispatch::Invoke(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr) { METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)->Invoke(dispidMember, riid, lcid,
wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr); } The last step is to implement the functions that are specific to your server. As can be seen in Listing 3.19, you simplified the implementation by calling the original MFC functions from the dual-interface implementations rather than reimplementing the functions using the new style. Doing so solves several problems. First, you can still rely on the MFC message mapping functions to invoke the methods when they are called via the IDispatch interface, and it does not require you to implement your own IDispatch code. Supporting the original MFC implementation also prevents you from having to change internal code that may rely on the already existing functions. As you will see a little later in this chapter, adding error handling to the dual-interface code is much simpler using this style.
Listing 3.19 TRACKER.CPP--ITracker Function Implementation ///////////////////////////////////////////////////////////////////////////// // CTracker interface handlers HRESULT CTracker::XSubDispatch::get_Indent(LONG * Indent) { METHOD_PROLOGUE(CTracker, SubDispatch) HRESULT hResult = S_OK; *Indent = pThis->GetIndent(); return hResult; } HRESULT CTracker::XSubDispatch::put_Indent(LONG Indent) { METHOD_PROLOGUE(CTracker, SubDispatch) HRESULT hResult = S_OK; pThis->SetIndent(Indent); return hResult; } HRESULT FAR EXPORT CTracker::XSubDispatch::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal) { METHOD_PROLOGUE(CTracker, SubDispatch) HRESULT hResult = S_OK; *RetVal = pThis->OutputLines(varOutputArray, varIndent); return hResult; } After compiling the server and registering it, you are ready to test its new dual-interface functionality. From C++, you will create the server as you did before, but now you may access its custom interface by using the QueryInterface function supplying the correct interface IID. From VB, you need only add the type library for the server to your list of references and change the name of the variable to the name that appears in the Object References dialog. You can also create the server using New instead of the CreateObject method. In VB, you call Dim MyTracker as TRACKER Set MyTracker = new TRACKER
instead of Dim MyTracker as Object Set MyTracker = CreateObject("MFCServer.Tracker") In C++, you call QueryInterface passing the IID ITracker interface IID. The last step in your dualinterface conversion is to handle errors correctly. As a general rule, a server cannot throw C++ exceptions from the custom interface implementation of a dual-interface server. For that matter, it can't throw them from any interface. MFC just does the job of catching the C++ exceptions for you and converting them to OLE exceptions that are understood by OLE. Since you are supporting a custom interface in your server, you must do the same.
Generating Dual-Interface OLE Exceptions Dual-interface rules for handling errors and exceptions is slightly different for the custom interface portion of the interface. As you saw earlier, all of the methods were changed to return an HRESULT in place of the function's normal return value. An HRESULT is used to indicate that an error or exception has occurred within the context of the method that was invoked. When an automation controller invokes a method in the custom interface of a server, it should check the return value of the function to see if it returned S_OK. If not, the controller has the option of checking to see whether the server supports extended error information via the ISupportErrorInfo interface. When a server that supports the ISupportErrorInfo interface creates an error, it does so by creating an IErrorInfo object containing the error information. MFC does not support the ISupportErrorInfo interface by default, so you must add it yourself. We've created a set of macros to aid in adding the ISupportErrorInfo interface to your Automation Server (see Listing 3.20). We've used the macros defined in the ACDUAL sample included with MFC to simplify the ISupportErrorInfo implementation.
Listing 3.20 ERRORINFOMACROS.H--ISupportErrorInfo Helper Macros // this code is based on the ACDUAL MFC\OLE sample application provided with Visual C++ ///////////////////////////////////////////////////////////////////// // DECLARE_DUAL_ERRORINFO expands to declare the ISupportErrorInfo // support class. It works together with DUAL_ERRORINFO_PART and // IMPLEMENT_DUAL_ERRORINFO defined below. #define DECLARE_DUAL_ERRORINFO() \ BEGIN_INTERFACE_PART(SupportErrorInfo, ISupportErrorInfo) \ STDMETHOD(InterfaceSupportsErrorInfo)(THIS_ REFIID riid); \ END_INTERFACE_PART(SupportErrorInfo) \ ///////////////////////////////////////////////////////////////////// // DUAL_ERRORINFO_PART adds the appropriate entry to the interface map // for ISupportErrorInfo, if you used DECLARE_DUAL_ERRORINFO. #define DUAL_ERRORINFO_PART(objectClass) \ INTERFACE_PART(objectClass, IID_ISupportErrorInfo, SupportErrorInfo) \ ///////////////////////////////////////////////////////////////////// // IMPLEMENT_DUAL_ERRORINFO expands to an implementation of // ISupportErrorInfo which matches the declaration in // DECLARE_DUAL_ERRORINFO. #define IMPLEMENT_DUAL_ERRORINFO(objectClass, riidSource) \
STDMETHODIMP_(ULONG) objectClass::XSupportErrorInfo::AddRef() \ { \ METHOD_PROLOGUE(objectClass, SupportErrorInfo) \ return pThis->ExternalAddRef(); \ } \ STDMETHODIMP_(ULONG) objectClass::XSupportErrorInfo::Release() \ { \ METHOD_PROLOGUE(objectClass, SupportErrorInfo) \ return pThis->ExternalRelease(); \ } \ STDMETHODIMP objectClass::XSupportErrorInfo::QueryInterface( \ REFIID iid, LPVOID* ppvObj) \ { \ METHOD_PROLOGUE(objectClass, SupportErrorInfo) \ return pThis->ExternalQueryInterface(&iid, ppvObj); \ } \ STDMETHODIMP objectClass::XSupportErrorInfo::InterfaceSupportsErrorInfo( \ REFIID iid) \ { \ METHOD_PROLOGUE(objectClass, SupportErrorInfo) \ return (iid == riidSource) ? S_OK : S_FALSE; \ } Include the ErrorInfoMacros.h file in the server source file, as in Listing 3.21.
Listing 3.21 TRACKER.CPP--ISupportErrorInfo Include File . . . #include "stdafx.h" #include "MFCServer.h" // error info support #include "ErrorInfoMacros.h" // ODL generated interface file #include "trackerinterface.h" . . . You need to add a macro and a helper function to the class declaration of the server (see Listing 3.22). The macro declares the ISupportErrorInfo interface, and the helper function is used to translate the exception into an IErrorInfo object.
Listing 3.22 TRACKER.H--ISupportErrorInfo Class Declaration . . . DECLARE_OLECREATE(CTracker) // add declaration of ISupportErrorInfo implementation // to indicate we support the OLE Automation error object DECLARE_DUAL_ERRORINFO() HRESULT CreateErrorInfo(CException * pAnyException, REFIID riidSource); // needed for dual interface support
BEGIN_INTERFACE_PART(SubDispatch, ITracker) . . . Now that you have your interface declaration, you need to add it to the interface map. You also need to add the implementation macro for the interface and add the implementation of the helper function (see Listing 3.23). The CreateErrorInfo function translates any exception into an IErrorInfo object. The function is based on code that is part of the ACDUAL MFC sample application included with MFC. The primary responsibility of the function is to convert COleDispatchExceptions into IErrorInfo objects. It can, however, deal with any exception that it is passed, but the level of information about the error is far less. After the exception has been translated, it is set as the error information object for the currently executing thread.
Listing 3.23 TRACKER.CPP--ISupportErrorInfo Interface Implementation BEGIN_INTERFACE_MAP(CTracker, CCmdTarget) INTERFACE_PART(CTracker, IID_ITracker, SubDispatch) DUAL_ERRORINFO_PART(CTracker) END_INTERFACE_MAP() IMPLEMENT_OLECREATE(CTracker, _T("MFCServer.Tracker"), 0x11C82947, 0x4edd, 0x11d0, 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d) // Implement ISupportErrorInfo to indicate we support the // OLE Automation error handler. IMPLEMENT_DUAL_ERRORINFO(CTracker, IID_ITracker) // this code is based on the ACDUAL MFC\OLE sample application provided with Visual C++ HRESULT CTracker::CreateErrorInfo(CException * pAnyException, REFIID riidSource) { ASSERT_VALID(pAnyException); // create an error info object ICreateErrorInfo * pcerrinfo; HRESULT hr = ::CreateErrorInfo(&pcerrinfo); // if we succeeded if(SUCCEEDED(hr)) { // dispatch exception? if(pAnyException->IsKindOf(RUNTIME_CLASS(COleDispatchException))) { // specific IDispatch style exception COleDispatchException * e = (COleDispatchException *) pAnyException; // set the return value to the error hr = e->m_scError; // Set up ErrInfo object pcerrinfo->SetGUID(riidSource); pcerrinfo->SetDescription(e->m_strDescription.AllocSysString()); pcerrinfo->SetHelpContext(e->m_dwHelpContext); pcerrinfo->SetHelpFile(e->m_strHelpFile.AllocSysString()); pcerrinfo->SetSource(e->m_strSource.AllocSysString()); } else if (pAnyException->IsKindOf(RUNTIME_CLASS(CMemoryException)))
{ // failed memory allocation hr = E_OUTOFMEMORY; // Set up ErrInfo object pcerrinfo->SetGUID(riidSource); CString cstrFileName(AfxGetAppName()); pcerrinfo->SetSource(cstrFileName.AllocSysString()); } else { // other unknown/uncommon error hr = E_UNEXPECTED; // Set up ErrInfo object pcerrinfo->SetGUID(riidSource); CString cstrFileName(AfxGetAppName()); pcerrinfo->SetSource(cstrFileName.AllocSysString()); } // QI for the IErrorInfo interface IErrorInfo * perrinfo; if(SUCCEEDED(pcerrinfo->QueryInterface(IID_IErrorInfo, (LPVOID *) &perrinfo))) { // set the error info object ::SetErrorInfo(0, perrinfo); // release the reference perrinfo->Release(); } // release the reference pcerrinfo->Release(); } // delete the exception pAnyException->Delete(); // return the error value return hr; } The last step is to update the custom interface methods to defer all exceptions to the helper function that you just added. The additional code needed is straightforward and simple to implement (see Listing 3.24). For each of the methods, you wrap the call to the basic implementation of the function with a try...catch block that will translate any exception into an IErrorInfo object and return the error code of the exception to the calling application.
Listing 3.24 TRACKER.CPP--Custom Interface Exception Handling Code ///////////////////////////////////////////////////////////////////////////// // CTracker interface handlers HRESULT CTracker::XSubDispatch::get_Indent(LONG * Indent) { METHOD_PROLOGUE(CTracker, SubDispatch) HRESULT hResult = S_OK; try
{ *Indent = pThis->GetIndent(); } catch(CException * pException) { hResult = pThis->CreateErrorInfo(pException, IID_ITracker); } return hResult; } HRESULT CTracker::XSubDispatch::put_Indent(LONG Indent) { METHOD_PROLOGUE(CTracker, SubDispatch) HRESULT hResult = S_OK; try { pThis->SetIndent(Indent); } catch(CException * pException) { hResult = pThis->CreateErrorInfo(pException, IID_ITracker); } return hResult; } HRESULT FAR EXPORT CTracker::XSubDispatch::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal) { METHOD_PROLOGUE(CTracker, SubDispatch) HRESULT hResult = S_OK; try { *RetVal = pThis->OutputLines(varOutputArray, varIndent); } catch(CException * pException) { hResult = pThis->CreateErrorInfo(pException, IID_ITracker); } return hResult; } You've covered all of the basics of ActiveX server creation and use by applications other than your own. What if you need to create and use the server from within the application in which it is defined? Or perhaps your application contains more than one server implementation, with only one server being exposed as a creatable object and with the remaining servers being created only in response to a valid method call in the exposed server. These are referred to as nested objects.
Server Instantiation Using C++ OLE is not the only method for creating and using Automation Servers. This chapter will show you how to instantiate Automation Servers using C++ syntax.
At times, instantiating and using Automation Servers is necessary from within the application in which they are defined. Take, for example, a case where an application contains three servers, only one of which is directly creatable by outside applications using OLE. The remaining two servers can be created by the exposed server using C++ and returned via a method call to another application, which then uses the server as though it was created via OLE. As we stated earlier in this chapter, in order for an application to be created by another application using OLE, the server must include the MFC macros DECLARE_OLECREATE and IMPLEMENT_OLECREATE. By removing or not including these macros, an application cannot be instantiated using standard OLE server creation mechanisms. That fact, however, does not prevent the server from being created using C++ and MFC. Note, though, that any OLE server can be created in this fashion, as opposed to just those that are not creatable through standard OLE mechanisms. MFC supports a facility for creating OLE servers using a helper class known as CRuntimeClass. The CRuntimeClass can be used to create servers that will be used internally to the application in which they are defined and externally as a return or parameter value supplied to another application. To support CRuntimeClass creation of objects, a class must define either the IMPLEMENT_DYNAMIC, IMPLEMENT_DYNCREATE, or the IMPLEMENT_SERIAL macros within their class implementation, which is true for any MFC class inherited for Cobject, and not just those classes that utilize OLE. The following listing is not included in the sample applications because of its simplicity. We have, however, used the CTracker server as our example server. To create a server using the CRuntimeClass, perform the following steps:
// create a CTracker runtime object CRuntimeClass * pRuntimeClass = RUNTIME_CLASS(CTracker); // create an CTracker OLE object CTracker * opTracker = (CTracker *) pRuntimeClass->CreateObject(); . . . // use the object in anyway that is appropriate for the application // finished with the object - destroy it delete opTracker; After an object is created, it can then also be passed to another application. MFC supports two functions for retrieving OLE interfaces from a running server: GetIDispatch and GetInterface. GetIDispatch is defined in the VC++ help file, but the GetInterface function is not. GetInterface accepts a single parameter of an IID of the interface that is being requested. GetInterface will not increment the reference count of the pointer that is returned. The GetIDispatch function gives you the option.
Problems Associated with Instantiating OLE Servers Using C++ Two problems arise when instantiating OLE components using C++: reference counting and inprocess versus out-of-process execution. For reference counting, the problems can be that too many or too few reference counts exist on the server being used. Reference counting problems can arise regardless of how the server is instantiated, with either C++ or OLE. The problem is really relative to using servers in one to many relationships. All of the applications that use the same server must AddRef and Release the server properly to prevent problems. Reference counting problems manifest themselves as either the server terminating before it should or not terminating when it should. Be sure to check that all reference counts are being incremented and decremented correctly
when creating and using objects in a one-to-many relationship or when a server is created and passed to another application. The next problem that can arise is far more subtle and easier to miss, although the effects can be dramatic and obvious and involve the server's execution model. A server is not guaranteed to execute in-process to the application that is using it--only with the application that created it.
Application A creates and uses an in-process server called Server 1. At some point, Application A creates an out-ofprocess server called Application B. Application A passes Server 1 to Application B. Server 1 will execute as an out-of-process server to Application B since the server was created in the process space of Application A. This is important to keep in mind when creating and using nested objects or shared objects since the performance differences between the two is so great. So far you've only looked at how to create individual instances of objects. Next you will look at how to share objects.
Shared Servers OLE defines a facility for sharing objects called the Running Object Table. Essentially, a shareable object will publish its CLSID and an IUnknown reference to itself in the Running Object Table. Any application that so desires can ask for the running instance of the object rather than create a new instance. Using shared objects is useful for applications that may need to work with a single running instance of an application rather than create multiple copies. The Tracker object is a perfect candidate for this kind of functionality. Multiple applications could use the same Tracker object to log information, thus saving on memory. The first step in enabling shared object support is to add a member variable to store the ID that will identify the object in the Running Object Table. This ID must be retained since it is used later in revoking the object from the Running Object Table when the object is destroyed. For the sample implementation, you add the variable as a public member of the class (see Listing 3.25).
Listing 3.25 TRACKER.H--Shared Object Member Variable Added to the CTracker Class . . . END_INTERFACE_PART(SubDispatch) public: DWORD m_dwRegister; protected: FILE * m_fileLog; . . . When registering a server in the Running Object Table, you use the CLSID of the CoClass object to identify the object in the table. Your implementation requires that you declare the CLSID of the CoClass object in the source file. The CLSID is copied from the TrackerInterface.h header file and added to the source file. The line before the CLSID should be the include file initguid.h; this file is needed to properly declare the CLSID and resolve it to the compiler.
Listing 3.26 TRACKER.CPP--CLSID Declaration
. . . static const IID IID_ITracker = { 0x11c82946, 0x4edd, 0x11d0, { 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d } #include DEFINE_GUID(CLSID_TRACKER,0x11C82947L,0x4EDD,0x11D0,0xBE,0xD8,0x00,0x40,0x05, 0x38,0x97,0x7D); BEGIN_INTERFACE_MAP(CTracker, CCmdTarget) . . . Next you add the code to the server that registers the object as running in the Running Object Table. Listing 3.27 shows the implementation of the CTracker shared object support in the constructor of the CTracker class. The specifics of your server will determine the exact location where you register it as running. For your implementation, the constructor is fine. Other implementations may be dependent on a particular state being reached in the server before registering the server. The decision is completely up to you and your specific implementation. In your implementation, the first step is to clear the member variable. The implementation is dependent on this member to identify whether the object was successfully registered as running or not. Next you retrieve the IUnknown reference for the object and, if successful, pass it along with an address of the member variable to the RegisterActiveObject function. You specify a strong registration that will result in an extra reference count on the server to keep it in memory. If you didn't register the server, you make sure to clear the member variable, just to be safe.
Listing 3.27 TRACKER.CPP--RegisterActiveObject Added to the CTracker Constructor . . . EnableAutomation(); // make sure that the application won't unload until the reference count is ::AfxOleLockApp(); // clear the member m_dwRegister = NULL; // QI for the IUnknown - remember no AddRef LPUNKNOWN pIUnknown = this->GetInterface(&IID_IUnknown); // if we have an IUnknown if(pIUnknown) { // register the clsid as an active object so other applications will get the same object if(::RegisterActiveObject(pIUnknown, CLSID_TRACKER,
ACTIVEOBJECT_STRONG, &m_dwRegister) != S_OK) // make sure that the reference is clear m_dwRegister = NULL; } // setup our timer resolution
m_lTimeBegin = timeBeginPeriod(1); m_lHiResTime = m_lLastHiResTime = timeGetTime(); . . . The last step is to call RevokeActiveObject to remove the server from the Running Object Table. This step is the most critical aspect of shared object support. Do not add this code to the destructor of your server, as it will never be called. The destructor is called in response to the destruction of the server, which results from the server reference count reaching 0. But since the server has an extra reference count from the RegisterActiveObject call, this state is never reached. To ensure that you properly remove the server from the table, your best course of action is to implement the revocation in the Release function of the IUnknown implementation of your server so that the server's reference count can be monitored. Listing 3.28 shows the implementation to revoke the object from the Running Object Table. A call is made to decrement the reference count of the server, and the return value is saved. The next step is to see if the server has been registered as running, which is implied through a non-zero value in the member variable. Then you check to see if the reference count is 1. If the reference count is 1, the object is ready to be destroyed since the only application now referencing the object is the Running Object Table. Before calling RevokeActiveObject, it is important to increment the reference count and clear the member variable. RevokeActiveObject will result in a recursive call to Release, and you don't want to destroy the object until you are finished with the first call to Release. After the RevokeActiveObject call returns, call the Release function one last time to actually destroy the object and remove it from memory.
Listing 3.28 TRACKER.CPP--RevokeActiveObject Added to the Server . . . ULONG FAR EXPORT CTracker::XSubDispatch::Release() { METHOD_PROLOGUE(CTracker, SubDispatch) // call the function and check the refcount long lRefCount = pThis->ExternalRelease(); // if we are registered as running and there is the only refcount left if(pThis->m_dwRegister && lRefCount == 1) { // bump our refcount up so we don't destroy ourselves until we are done pThis->ExternalAddRef(); // get the registration ID DWORD tdwRegister = pThis->m_dwRegister; // clear the member variable to prevent us from hitting this method again pThis->m_dwRegister = 0; // remove the interface from the running object table ::RevokeActiveObject(tdwRegister, NULL); // the revoke should have decremented our refcount by one // this call to Release should destroy our server return pThis->ExternalRelease(); } // exit return lRefCount; } . . .
During the lifetime of the server, you can get the same instance of the server and use it from multiple applications. In VB, getting a running instance of a server is done with the GetObject call, and in VC++, with the GetActiveObject function. After the pointer to the server is retrieved, the server can be used as though it was created through normal OLE mechanisms. This method of sharing objects is fine but requires that the application using the server take an active role in deciding to use the shared object versus an application creating its own instance of the object. Another approach can be taken: You can supply the instance of a running server to an application that calls CreateObject, rather than relying on an application to call GetObject. This approach is known as a single instance server.
Single Instance Servers To support single instance servers, it is necessary to perform all of the steps described earlier in this chapter, in the section "Shared Servers," from within the ClassFactory of the server and not from within the implementation of the server itself. By implementing the object sharing code within the class factory, you are able to control the number of instances of the server without having to rely on the user of the server to program specifically for those cases. Unfortunately, MFC does not provide simple access to the COleObjectFactory class, which is responsible for creating OLE servers to allow for this kind of implementation. C++ inheritance, however, allows you to create a new specialized version of the COleObjectFactory class that can support server instance sharing. We created the class COleObjectFactoryShared and several macros to speed up your development. As can be seen in Listing 3.29, we created a new class derived from the original MFC class COleObjectFactory. The class contains a constructor and an interface map that routes messages from the IClassFactory and IClassFactory2 interfaces to the implementation. Last is a member variable that is used to store the ID of the server after it has been loaded into the Running Object Table. Two macros must be added to the server class declaration and implementation to enable shared server support. DECLARE_OLECREATE_SHARED must replace the DECLARE_OLECREATE macro in the class header file, and IMPLEMENT_OLECREATE_SHARED must replace IMPLEMENT_OLECREATE in the server source file. The only difference between the new macros and the originals is that a class factory of type COleObjectFactoryShared will be added to the server implementation rather than a COleObjectFactory class.
Listing 3.29 SHAREDOBJECT.H--Shared Server Class Factory Header File // // Shared Server Class Factory // // Code based on COleObjectFactory (c) 1996,1997 Microsoft Corp // class COleObjectFactoryShared: public COleObjectFactory { DECLARE_DYNAMIC(COleObjectFactoryShared) // Construction public: COleObjectFactoryShared(REFCLSID clsid, CRuntimeClass* pRuntimeClass, BOOL bMultiInstance, LPCTSTR lpszProgID); // Interface Maps
public: DECLARE_INTERFACE_MAP() BEGIN_INTERFACE_PART(SubClassFactory, IClassFactory2) STDMETHOD(CreateInstance)(LPUNKNOWN, REFIID, LPVOID*); STDMETHOD(LockServer)(BOOL); STDMETHOD(GetLicInfo)(LPLICINFO); STDMETHOD(RequestLicKey)(DWORD, BSTR*); STDMETHOD(CreateInstanceLic)(LPUNKNOWN, LPUNKNOWN, REFIID, BSTR, LPVOID*); END_INTERFACE_PART(SubClassFactory) public: DWORD m_dwRegister; }; ///////////////////////////////////////////////////////////////////////////// // Macros for creating "creatable and shareable" automation classes. #define DECLARE_OLECREATE_SHARED(class_name) \ public: \ static AFX_DATA COleObjectFactoryShared factory; \ static AFX_DATA const GUID guid; #define IMPLEMENT_OLECREATE_SHARED(class_name, external_name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ AFX_DATADEF COleObjectFactoryShared class_name::factory(class_name::guid, \ RUNTIME_CLASS(class_name), FALSE, _T(external_name)); \ const AFX_DATADEF GUID class_name::guid = \ { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }; Listing 3.30 shows the implementation of the COleObjectFactoryShared class. The constructor defers to the base class and clears the m_dwRegister member variable. As with the previous section, the m_dwRegsiter variable is crucial to the success of the shared server support. Next you route all messages from the IClassFactory and IClassFactory2 interfaces to the implementation of the functions. All but two of the functions defer to the base class implementation without modification. CreateInstance and CreateInstanceLic first check to see if an instance of the server is already running. If not, the functions attempt to create the server using the base class implementation of CreateInstanceLic; if so, the functions register the server as running. If the server was already running, the function returns the IUnknown of the existing object. Remember that when the server is registered as running, it has an extra reference count. The code to manage the release of the server correctly will remain almost unchanged from the implementation that you created earlier in this chapter.
Listing 3.30 SHAREDOBJECT.CPP--Shared Server Implementation File // // Shared Server Class Factory // // Code based on COleObjectFactory (c) 1996,1997 Microsoft Corp // #include "stdafx.h" #include "sharedobject.h" IMPLEMENT_DYNAMIC(COleObjectFactoryShared, COleObjectFactory) COleObjectFactoryShared::COleObjectFactoryShared(REFCLSID clsid, CRuntimeClass* pRuntimeClass, BOOL bMultiInstance, LPCTSTR lpszProgID)
:COleObjectFactory(clsid, pRuntimeClass, bMultiInstance, lpszProgID) { // clear the registered server ID member m_dwRegister = NULL; } BEGIN_INTERFACE_MAP(COleObjectFactoryShared, COleObjectFactory) INTERFACE_PART(COleObjectFactoryShared, IID_IClassFactory, SubClassFactory) INTERFACE_PART(COleObjectFactoryShared, IID_IClassFactory2, END_INTERFACE_MAP() ///////////////////////////////////////////////////////////////////////////// // Implementation of COleObjectFactoryShared::IClassFactory interface STDMETHODIMP_(ULONG) COleObjectFactoryShared::XSubClassFactory::AddRef() { METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory) return pThis->m_xClassFactory.AddRef(); } STDMETHODIMP_(ULONG) COleObjectFactoryShared::XSubClassFactory::Release() { METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory) return pThis->m_xClassFactory.Release(); } STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::QueryInterface( REFIID iid, LPVOID* ppvObj) { METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory) return pThis->m_xClassFactory.QueryInterface(iid, ppvObj); } STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::CreateInstance( IUnknown* pUnkOuter, REFIID riid, LPVOID* ppvObject) { METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory) HRESULT hResult = S_OK; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // see if the object is already running ::GetActiveObject(riid, NULL, &pIUnknown); // if we didn't get a reference to a running object if(!pIUnknown) { // create the server hResult = pThis->m_xClassFactory.CreateInstanceLic(pUnkOuter, NULL, riid, NULL, ppvObject); // if successful if(hResult == S_OK) { // register the clsid as an active object so // other applications will get the same object if(::RegisterActiveObject((IUnknown*)*ppvObject, riid, ACTIVEOBJECT_STRONG, &pThis->m_dwRegister) != S_OK) // clear the ID member pThis->m_dwRegister = NULL;
} else // clear the ID member pThis->m_dwRegister = NULL; } else // use the running object *ppvObject = pIUnknown; return hResult; } STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::LockServer(BOOL fLock) { METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory) return pThis->m_xClassFactory.LockServer(fLock); } STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::GetLicInfo( LPLICINFO pLicInfo) { METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory) return pThis->m_xClassFactory.GetLicInfo(pLicInfo); } STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::RequestLicKey( DWORD dwReserved, BSTR* pbstrKey) { METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory) return pThis->m_xClassFactory.RequestLicKey(dwReserved, pbstrKey); } STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::CreateInstanceLic( LPUNKNOWN pUnkOuter, LPUNKNOWN pUnkReserved, REFIID riid, BSTR bstrKey, LPVOID* ppvObject) { METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory) HRESULT hResult = S_OK; // clear the ID member pThis->m_dwRegister = NULL; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // see if the object is already running ::GetActiveObject(riid, NULL, &pIUnknown); // if we didn't get a reference to a running object if(!pIUnknown) { // create the server hResult = pThis->m_xClassFactory.CreateInstanceLic(pUnkOuter, pUnkReserved, riid, bstrKey, ppvObject); // if successful if(hResult == S_OK) { // register the clsid as an active object // so other applications will get the same object
if(::RegisterActiveObject((IUnknown*)*ppvObject, riid, ACTIVEOBJECT_STRONG, &pThis->m_dwRegister) != S_OK) // clear the ID member pThis->m_dwRegister = NULL; } } else // use the running object *ppvObject = pIUnknown; return hResult; } Now that the infrastructure is in place to support shared servers, you need to add the new code to your application. First the header file must be updated (see Listing 3.31). Replace the macro DECLARE_OLECREATE with the new macro DECLARE_OLECREATE_SHARED. Since the sample application was used for the section on how to implement shared servers, we have also commented out the registered server ID member variable, m_dwRegister.
Listing 3.31 TRACKER.H--Shared Server Class Factory Support Added to the Class Definition . . . afx_msg BOOL OutputLines(VARIANT FAR* varOutputArray, const VARIANT FAR& varIndent); //}}AFX_DISPATCH DECLARE_DISPATCH_MAP() DECLARE_INTERFACE_MAP() // DECLARE_OLECREATE(CTracker) DECLARE_OLECREATE_SHARED(CTracker) // add declaration of ISupportErrorInfo implementation // to indicate we support the OLE Automation error object DECLARE_DUAL_ERRORINFO() HRESULT CreateErrorInfo(CException * pAnyException, REFIID riidSource); // needed for dual interface support BEGIN_INTERFACE_PART(SubDispatch, ITracker) STDMETHOD(GetTypeInfoCount)(THIS_ UINT FAR* pctinfo); STDMETHOD(GetTypeInfo)(THIS_ UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo); STDMETHOD(GetIDsOfNames)(THIS_ REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid, DISPID FAR* rgdispid); STDMETHOD(Invoke)(THIS_ DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr); STDMETHOD(get_Indent)(THIS_ long FAR* Value); STDMETHOD(put_Indent)(THIS_ long Value); STDMETHOD(OutputLines)(THIS_ VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal); END_INTERFACE_PART(SubDispatch) //public:
// DWORD m_dwRegister; protected: FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime; long m_lIndent; }; Next you must update the source file for your server (see Listing 3.32). Add the include file SharedObject.h to resolve the macros and class name. In the sample, we commented out the section of code within the constructor that registered the server as running. You must also replace the macro IMPLEMENT_OLECREATE with IMPLEMENT_OLECREATE_SHARED.
Listing 3.32 TRACKER.CPP--Shared Server Update to Class Implementation . . . #include "stdafx.h" #include "MFCServer.h" // shared object support #include "sharedobject.h" // error info support #include "ErrorInfoMacros.h" // ODL generated interface file CTracker::CTracker() { EnableAutomation(); // make sure that the application won't unload until the reference count is ::AfxOleLockApp(); // clear the member /* m_dwRegister = NULL; // QI for the IUnknown - remember no AddRef LPUNKNOWN pIUnknown = this->GetInterface(&IID_IUnknown); // if we have an IUnknown if(pIUnknown) { // register the clsid as an active object so other applications // will get the same object if(::RegisterActiveObject(pIUnknown, CLSID_TRACKER, ACTIVEOBJECT_STRONG, &m_dwRegister) != S_OK) // make sure that the reference is clear m_dwRegister = NULL; } */ // setup our timer resolution m_lTimeBegin = timeBeginPeriod(1); m_lHiResTime = m_lLastHiResTime = timeGetTime(); // get the current date and time CTime oTimeStamp = CTime::GetCurrentTime();
CString cstrFileName; // create a file name based on the date cstrFileName.Format(_T("%s.tracklog"), (LPCTSTR) oTimeStamp.Format("%Y%m%d")); // open a file m_fileLog = fopen(cstrFileName, _T("a")); // if we have a file handle if(m_fileLog) { // output some starting information fprintf(m_fileLog, _T("************************\n")); fprintf(m_fileLog, _T("Start %s\n"), (LPCTSTR) oTimeStamp.Format("%B %#d, %Y, %I:%M %p")); fprintf(m_fileLog, _T("\n")); } m_lIndent = 0; } . . . IMPLEMENT_OLECREATE_SHARED(CTracker, _T("MFCServer.Tracker"), 0x11C82947, 0x4edd, 0x11d0, 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d) The last step is to add the code to revoke the server from the Running Object Table (see Listing 3.33). As with the previous section, the management of the server's lifetime is dependent on its reference count and is handled within the Release function implementation. The only difference between this implementation and the shared object implementation is the location of the m_dwRegister member variable, which is now located in the COleObjectFactoryShared class.
Listing 3.33 TRACKER.CPP--Shared Server Release Implementation ULONG FAR EXPORT CTracker::XSubDispatch::Release() { METHOD_PROLOGUE(CTracker, SubDispatch) // call the function and check the refcount long lRefCount = pThis->ExternalRelease(); // if we are registered as running and there is the only refcount left // if(pThis->m_dwRegister && lRefCount == 1) if(pThis->factory.m_dwRegister && lRefCount == 1) { // bump our refcount up so we don't destroy ourselves until we are done pThis->ExternalAddRef(); // get the registration ID // DWORD tdwRegister = pThis->m_dwRegister; DWORD tdwRegister = pThis->factory.m_dwRegister; // clear the member variable to prevent us from hitting this method again // pThis->m_dwRegister = 0; pThis->factory.m_dwRegister = 0; // remove the interface from the running object table ::RevokeActiveObject(tdwRegister, NULL); // the revoke should have decremented our refcount by one // this call to Release should destroy our server
return pThis->ExternalRelease(); } // exit return lRefCount; } The implementation and use of shared and single instance server support is straightforward and adds a level of functionality not normally found in standard server implementations.
From Here... In this chapter, you learned how to create a basic implementation of an MFC Automation Server. You also learned how to expand upon the basic framework provided by MFC to create new and interesting features within your implementation. Other areas where your server development has room to expand are in adding User Interface, possibly in the form of dialogs and event interfaces. The use of the basic MFC dialog classes can make implementation of UI a very easy and rewarding part of your implementations. At the time of this writing, no container applications will recognize either of these features within an Automation Server. If your implementation has these requirements, you must decide how to implement them. The creation of services and remote servers also makes the prospect of implementing Automation Servers very enticing. Every day more and more developers are enabling or integrating automation support as a basic feature of their applications. Any situation that calls for the transfer of data among applications can now be performed with OLE automation, whereas before data transfer would have required the exchange of data using DDE or, even worse, file import/export functions. Automation Servers provide an easy and flexible way to create lightweight ActiveX components for use by your applications. The support of both IDispatch interfaces and custom interfaces (a Dual-Interface Server) also gives the user of the server a lot of flexibility in terms of implementation styles and methods. Chapter 4 looks at how to create an Automation Server using the ActiveX Template Library (ATL).
Chapter 4 Creating ActiveX Automation Servers Using ATL ●
Creating ActiveX Automation Servers Using ATL ❍ Creating the Basic Project ❍ Adding an Automation Interface to the Application ■ Listing 4.1 ATLSERVER.IDL--IDL File for the ATL Server Sample ❍ Registry ■ Listing 4.2 ATLCONTROLWIN.RGS--Sample Registry Script File for theCTracker Server Object ■ Listing 4.3 TRACKER.RGS--Updated Tracker.rgs File ❍ Sample Server Support Code ■ Listing 4.4 Tracker.H--Sample Server Support Code Added to the Tracker Header File ■ Listing 4.5 TRACKER.CPP--Sample Server Support Code Added to the Source File ❍ Adding Methods ■ Listing 4.6 ATLSERVER.IDL--OutputLines Method Added to the IDL File by the ATL ClassWizard ■ Listing 4.7 TRACKER.H--OutputLines Function Prototype Added to the ■ Tracker.h File ■ Listing 4.8 TRACKER.CPP--OutputLines Function Implementation Added to the Tracker.cpp File ■ Listing 4.9 TRACKER.H--m_1Indent Member Variable Added to the Class Definition ■ Listing 4.10 TRACKER.CPP--Member Initialization in the Constructor ■ Listing 4.11 TRACKER.CPP--OutputLines Function Implementation Added to the Source File Tracker.cpp ❍ Adding Properties ■ Listing 4.12 TRACKER.IDL--Indent Property Added to the IDL ■ Listing 4.13 TRACKER.H--Indent Property Function Pair Prototypes Added to the Tracker.h File ■ Listing 4.14 TRACKER.CPP--Indent Property Function Pair Implementation Added to the Tracker.cpp File ■ Listing 4.15 TRACKER.CPP--Indent Property Implementation ❍ Generating OLE Exceptions ■ Listing 4.16 ATLSERVER.IDL--Error Enumeration Added to the IDL File ■ Listing 4.17 TRACKER.CPP--Rich Error Information Added to Tracker ■ Implementation ❍ Dual-Interface ❍ Generating Dual-Interface OLE Exceptions ❍ Server Instantiation Using C++ ❍ Shared Servers ■ Listing 4.18 STDAFX.H--Shared Object Support Classes and Macros ■ Listing 4.19 TRACKER.H--Shared Server Support Added to Ctracker Class ❍ Single Instance Servers
Listing 4.20 STDAFX.G--Single Instance Server Support Added to StdAfx.h ■ Listing 4.21 TRACKER.H--Single Instance Server Support Added to CTracker Class From Here... ■
❍
Creating ActiveX Automation Servers Using ATL ●
●
●
●
●
●
Server registration support Automation servers depend on the registry for information about how they are implemented and used. Adding methods and properties Methods allow the user of the server to access specific functionality in the server. Properties provide a uniform mechanism for getting and setting the state of a server's data variables. OLE exceptions Unlike MFC, ATL does not support OLE exceptions through a C++ exception; rather it takes advantage of OLE error mechanisms. Dual-interface ATL servers implement dual- interface as a standard feature of the application. Creating servers using C++ C++ can be used to launch servers from the application in which they are defined. Shared and single instance servers Using a server that is already running is usually the choice of the controller of the server, not the implementer. With a single instance server, the server implementer is responsible for the reuse of a server that is already running.
The ActiveX Template Library (ATL) was created to answer the need for lightweight and fast ActiveX COM components. In addition to creating COM Objects and Controls, ATL can be used to create Automation Servers with a minimum of effort and overhead. ATL consists of a set of template classes that are a relatively new concept to most developers; however, the focus of this chapter is on creating Automation Servers. Please see your favorite C++ book or manual for more information regarding the definition and use of templates. ATL is intended to solve the problems of COM com-ponent development and is not an attempt to be an allencompassing class library for creating applications. ATL was designed to work with other class libraries, such as MFC or the Standard Template Library (STL), that provide basic classes for string manipulation, array, lists, memory management, and so on. You, the developer, have the freedom to choose which class library best suits your needs and combine it with ATL to create small, fast COM applications. In this chapter, you will create a simple in-process Automation Server using ATL and MFC for logging string data to a file. The use of MFC will allow you to concentrate on how to implement your server rather than on how to find alternative methods to functions and classes you are familiar with. You could just as easily opt not to use MFC, but then you would have to use the Windows API directly to implement anything related to the OS. As it is, using MFC will not add much to your application in terms of size.
As you proceed through the chapter, you will expand on your implementation, highlighting some of the more advanced concepts of Automation Server creation using ATL.
Creating the Basic Project When creating an Automation Server, the first step is to create a basic project upon which you will build your application's features and functionality. Like MFC, ATL has an AppWizard for creating the basic ATL project. From the File menu, select the New menu item. In the New dialog (see fig. 4.1), select the Projects tab. The Projects tab allows you the opportunity to define several aspects of how the application will be created, for example, the type of application to create, the name of the application, and the location where you want the project created. For the type, select ATL COM AppWizard; enter the Project name ATLServer, and the Location will be C:\que\ActiveX\ ATLServer. Click the OK button to start the ATL COM AppWizard so you can further define the properties of your server. FIG. 4.1 Define the new ATL server project in the New dialog. In the ATL COM AppWizard -- Step 1 of 1 dialog (see fig. 4.2), select a Server Type of Dynamic Link Library (DLL), and check the Support MFC check box. Click the Finish button to continue. FIG. 4.2 Define the basic architecture of the ATL COM object with the ATL COM AppWizard. The New Project Information dialog (see fig. 4.3) is used to confirm the settings that were selected for the project prior to the creation of the actual source files. This is the last step in the ATL COM AppWizard. "But wait," you say, "I haven't defined any of my server properties." The ATL COM AppWizard takes a slightly different approach from that of MFC. Only the basic source files are created with the AppWizard, the remainder of the project is defined by the ATL ObjectWizard--thus allowing for much better control of the project implementation versus MFC since the developer can add any number of ActiveX Servers, Controls, or plain COM objects after the basic project is created. After you have confirmed your project settings, click the OK button to close the ATL COM AppWizard and create the ATLServer project. FIG. 4.3 Confirm the new project settings with the New Project Information dialog. The ATL AppWizard generates all of the basic files that are needed to create a DLL-based ATL Automation Server. Table 4.1 lists all of the files that are created and a brief explanation of their purpose. Table 4.1 Basic Source Files Created by the ATL AppWizard Filename
Description
ATLServer.clw
VC++ project file.
ATLServer.cpp
The main application source file and entry point for the DLL.
ATLServer.def
Standard application DEF file. This file contains the function export declarations needed for all in-process servers.
ATLServer.dsp
VC++ project file.
ATLServer.dsw VC++ project file. ATLServer.idl
Interface Definition Language file, which is used to create the type library for the server.
ATLServer.ncb
VC++ project file.
ATLServer.rc
Standard resource file.
StdAfx.cpp
Standard precompiled header source file.
ATLServerps.def Proxy/Stub DLL definition file.
ATLServerps.mk Proxy/Stub MK file. StdAfx.h
Standard precompiled header file. All of the MFC-specific include files are added here.
Adding an Automation Interface to the Application To be an Automation Server, an application must contain at least one or more IDispatch-based interfaces. You will use the ATL ObjectWizard to add your automation server interfaces to your application. From the Insert menu, select the New ATL Object menu item. Within the ATL ObjectWizard dialog (see fig. 4.4), select the Objects item in the left panel to display the types of ATL components that can be added. Your implementation will be an Automation Server, so select the Simple Object icon. See the ATL documentation for more information on the other types of objects that can be created. Click the Next button to continue. FIG. 4.4 Select the type of ATL object to add to your project. The next dialog is the ATL Object Wizard Properties dialog, which is used to define the specific properties of the new object that will be added to your project. Select the Names tab (see fig. 4.5), and in the Short Name edit field, type Tracker; the remainder of the edit fields will automatically update, reflecting the short name that you added. The other fields can be changed, but in this case, you will use the default values. FIG. 4.5 Define the name of the new control object. Select the Attributes tab so that you can define the attributes of the server object (see fig. 4.6). Check the Support ISupportErrorInfo check box to add OLE rich error. You can add events to the ATL object by checking the Support Connection Points check box; however, for the purposes of the sample, you will not. Leave the remainder of the settings on the Attributes tab at their default values. Click OK to continue and to add the object to the project. FIG. 4.6 Define the attributes of the new server object. Chapters 8 and 9 go into detail about adding events to an ATL control; the same process is used for an ATL server. At the time this book was being written, one drawback to having events was that only VB5 was built to take advantage of them. The ATL ObjectWizard added the files, Tracker.h, Tracker.cpp, and Tracker.rgs to the project. Tracker.h and Tracker.cpp are the implementation files for your server object. Tracker.rgs is the registry script file that is used to register your server in the registration database. You will learn more about the Tracker.rgs file in the section regarding server registration a little later in this chapter. Before continuing with your server implementation, your newly created IDL file deserves a quick review. Listing 4.1 shows the basic IDL file that is generated by the AppWizard.
TIP: For clarity, you should add the hidden attribute to the IDispatch-based interfaces defined in the server (see Listing 4.1). This step prevents tools like VB from displaying both the IDispatch interface and its related CoClass within the VB Object browser. The CoClass is the only interface that needs to be visible since it is the only interface that VB uses. Using the
IDispatch interface in VB will result in errors.
The most obvious difference between the ODL from the MFC sample and the IDL of this sample is the location of the IDispatch interface relative to the rest of the type library description. When using IDL, you must declare the interfaces that will generate the C++ source files outside of the library declaration. For the ODL, this step is not necessary. Other than a few minor language differences, the IDL and ODL are identical in terms of syntax and organization.
Listing 4.1 ATLSERVER.IDL--IDL File for the ATL Server Sample import "oaidl.idl"; // ATLServer.idl : IDL source for ATLServer.dll // // This file will be processed by the MIDL tool to // produce the type library (ATLServer.tlb) and marshalling code. [ object, uuid(03699612-809E-11D0-BEFF-00400538977D), dual, helpstring("ITracker Interface"), pointer_default(unique), hidden ] interface ITracker : IDispatch { }; [ uuid(03699601-809E-11D0-BEFF-00400538977D), version(1.0), helpstring("ATLServer 1.0 Type Library") ] library ATLSERVERLib { importlib("stdole32.tlb"); [ uuid(03699613-809E-11D0-BEFF-00400538977D), helpstring("Tracker Class") ] coclass Tracker { [default] interface ITracker; }; }; Before another application can use the server, however, OLE has to know where to find the server, which is done through the system registry. All ActiveX components that are publicly available to other applications must support registration and must create valid registry entries.
Registry ActiveX components have one or more registry entries that are used to describe various aspects of the application and how it can be used. The registry is critical to the successful launching and using of ActiveX components.
All inproc ActiveX components expose registration support via two exported functions: DllRegisterServer and DllUnregisterServer. For information regarding the registration of local servers, see Chapter 3. The basic registration and unregistration support for the server is already implemented by ATL. You are not required to make code changes or additions to support it. Remember that the MFC implementation allows only for registering the server and does not support unregistration. Unlike MFC which uses a set of constants, ATL relies on resource information in the form of a registry script file to define the information that is added to the registry database. The registry script file is added automatically to the project when the server object is added; there is one script file for each server object. The registry script file(s) are compiled into the server project as resources and can be viewed in binary form in the resource editor. The files, which have the extension .rgs, are normal text files that can be edited within the IDE. For more information about the use of registry script files and their particular syntax, see the VC++ books online subject "Registry Scripting Examples--ActiveX Template Library, Articles." Listing 4.2 shows the registry script file for the CTracker server object that you added.
Listing 4.2 ATLCONTROLWIN.RGS--Sample Registry Script File for the CTracker Server Object HKCR { Tracker.Tracker.1 = s `Tracker Class' { CLSID = s `{03699613-809E-11D0-BEFF-00400538977D}' } Tracker.Tracker = s `Tracker Class' { CurVer = s `Tracker.Tracker.1' } NoRemove CLSID { ForceRemove {03699613-809E-11D0-BEFF-00400538977D} = s `Tracker Class' { ProgID = s `Tracker.Tracker.1' VersionIndependentProgID = s `Tracker.Tracker' ForceRemove `Programmable' LocalServer32 = s `%MODULE%' } } }
NOTE: Unfortunately, at the time of the writing of this book, the ATL ClassWizard created the Tracker.rgs file incorrectly. Hopefully, this problem will be fixed by the time the product is released. Listing 4.3 shows the updated version of the Tracker.rgs file with the mistakes corrected. The first correction was to change the ProgID to ATLServer.Tracker, reflecting the parent module and the subordinate object. The next correction was to add the CLSID entry to the version- independent ProgID section. This may have been intentional on the part of the ATL team. The last and definitely most significant problem was the fact that the CLSID section listed the server as
LocalServer32 and not InprocServer32. After making the corrections, the server registered and ran as expected.
Listing 4.3 TRACKER.RGS--Updated Tracker.rgs File HKCR { ATLServer.Tracker.1 = s `Tracker Class' { CLSID = s `{03699613-809E-11D0-BEFF-00400538977D}' } ATLServer.Tracker = s `Tracker Class' { CLSID = s `{03699613-809E-11D0-BEFF-00400538977D}' CurVer = s `ATLServer.Tracker.1' } NoRemove CLSID { ForceRemove {03699613-809E-11D0-BEFF-00400538977D} = s `Tracker { ProgID = s `ATLServer.Tracker.1' VersionIndependentProgID = s `ATLServer.Tracker' ForceRemove `Programmable' InprocServer32 = s `%MODULE%' } } }
Sample Server Support Code Since the server is used to output data to a file, you first need to add some support code to the application before adding its methods and properties. Listing 4.4 shows the changes and additions that need to be made to the class header file. First add a destructor to the class and remove the constructor implementation--you will add it to the source file later. Then add a set of member variables for storing the file handle and timer information that will be used throughout the server implementation.
Listing 4.4 Tracker.H--Sample Server Support Code Added to the Tracker Header File ///////////////////////////////////////////////////////////////////////////// // CTracker class ATL_NO_VTABLE CTracker : public CComObjectRootEx, public CComCoClass, public ISupportErrorInfo, public IDispatchImpl {
public: // constructor CTracker(); // destructor ~CTracker(); DECLARE_REGISTRY_RESOURCEID(IDR_TRACKER) BEGIN_COM_MAP(CTracker) COM_INTERFACE_ENTRY(ITracker) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(ISupportErrorInfo) END_COM_MAP() // ISupportsErrorInfo STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker public: protected: FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime; }; The next step is to update the source file for the class. Add the include file, mmsystem.h, before the Tracker.h include file (see Listing 4.5). This file is for the timer functions that you take advantage of throughout the server implementation. The following line is added within the constructor:
AFX_MANAGE_STATE(AfxGetStaticModuleState()); If you are using MFC with ATL, you must add this line as the first line in every function that uses MFC. This line is needed to stabilize the state information that is used by MFC. If you do not perform this step, the server will not function correctly and can result in errors during program execution. You must call the method AfxOleLockApp() to ensure that the application will not be unloaded from memory until the reference count reaches zero. Next you create a high resolution timer and store its current value in your member variables. The timer is useful for determining the number of milliseconds that have passed since the last method call was made. The timer output is great for tracking the performance of a particular action or set of actions. You then get the current date and create a filename with the format YYYYMMDD.tracklog. After successfully opening the file, you output some start-up data to the file and exit the constructor. The destructor does the exact opposite of the constructor. If there is a valid file handle, you write some closing information to the file and close it. Next you terminate the timer. Remember to call the function AfxOleUnlockApp() to allow the application to be removed from memory.
Listing 4.5 TRACKER.CPP--Sample Server Support Code Added to the Source File // Tracker.cpp : Implementation of CTracker #include "stdafx.h" #include "ATLServer.h" // needed for the high resolution timer services
#include #include "Tracker.h" ///////////////////////////////////////////////////////////////////////////// // CTracker STDMETHODIMP CTracker::InterfaceSupportsErrorInfo(REFIID riid) { static const IID* arr[] = { &IID_ITracker, }; for (int i=0;ivt == (VT_ARRAY | VT_BSTR)) { // lock the array so we can use it if(::SafeArrayLock(varOutputArray->parray) == S_OK) { LONG lLBound; // get the lower bound of the array if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK) { LONG lUBound; // get the number of elements in the array if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound) == S_OK) { CString cstrIndent; CTime oTimeStamp; BSTR bstrTemp; // if we have an indent parameter if(varIndent.vt != VT_I4) { // get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful // - VariantChangeTypeEx() could also be used if(S_OK == :VariantChangeType(&varConvertedValue, (VARIANT *) &varIndent, 0, VT_I4)) // assign the value to our member variable m_lIndent = varConvertedValue.lVal; } else // assign the value to our member variable m_lIndent = varIndent.lVal; // if we have to indent the text for(long lIndentCount = 0; lIndentCount < m_lIndent; lIndentCount++) // add a tab to the string cstrIndent += _T("\t"); // for each of the elements in the array
for(long lArrayCount = lLBound; lArrayCount < (lUBound + lLBound); lArrayCount++) { // update the time oTimeStamp = CTime::GetCurrentTime(); m_lHiResTime = timeGetTime(); // get the data from the array if(::SafeArrayGetElement(varOutputArray->parray, &lArrayCount, &bstrTemp) == S_OK) { // output the data fprintf(m_fileLog, _T("%s(%10ld)-%s%ls\n"), (LPCTSTR) oTimeStamp.Format ("%H:%M:%S"), m_lHiResTime - m_lLastHiResTime, (LPCTSTR) cstrIndent, bstrTemp); // store the last timer value m_lLastHiResTime = m_lHiResTime; // free the bstr ::SysFreeString(bstrTemp); } } } else *RetVal = VARIANT_FALSE; } else *RetVal = VARIANT_FALSE; // unlock the array we don't need it anymore ::SafeArrayUnlock(varOutputArray->parray); } else *RetVal = VARIANT_FALSE; } else *RetVal = VARIANT_FALSE; // return the result return hResult; } Now you have added a method. In the following section, you will learn how to implement its counterpart, the property.
Adding Properties A property can be thought of as an exposed variable that is defined in the Automation Server. Properties are useful for setting and retrieving information about the state of the server. The m_lIndent member variable that you added to the class definition is a perfect candidate to be exposed as a property. Properties are added in much the same way as methods. From the ClassView tab in the Project Workspace window, select the ITracker class, click the right mouse button, and select the Add Property menu item (see fig.
4.9). FIG. 4.9 Add a new property to the Server with the ATL ClassWizard. In the Add Property to Interface dialog, set the Property Type to long, type the Property Name as Indent, and leave the remainder of the settings at their default values (see fig. 4.10). Click OK to confirm the entry and close the dialog. FIG. 4.10 Define the Indent property attributes. Like the OutputLines method, the ATL ClassWizard added entries to the IDL file (see Listing 4.12), the Tracker.h header file (see Listing 4.13), and the Tracker.cpp source file (see Listing 4.14) to support the new property. As in Chapter 3, properties are added as a pair of related functions, and the same is true for the ATL server and ActiveX components.
Listing 4.12 TRACKER.IDL--Indent Property Added to the IDL . . . interface ITracker : IDispatch { [id(1), helpstring("method OutputLines")] HRESULT OutputLines( [in] VARIANT * varOutputArray, [in,optional] VARIANT varIndent, [out,retval] VARIANT_BOOL * RetVal); [propget, id(2), helpstring("property Indent")] HRESULT Indent( [out, retval] long *pVal); [propput, id(2), helpstring("property Indent")] HRESULT Indent( [in] long newVal); }; . . .
Listing 4.13 TRACKER.H--Indent Property Function Pair Prototypes Added to the Tracker.h File . . . // ITracker public: STDMETHOD(get_Indent)(/*[out, retval]*/ long *pVal); STDMETHOD(put_Indent)(/*[in]*/ long newVal); STDMETHOD(OutputLines)(/*[in]*/ VARIANT * varOutputArray, /*[in,optional]*/ VARIANT varIndent, /*[out,retval]*/ VARIANT_BOOL * RetVal); . . .
Listing 4.14 TRACKER.CPP--Indent Property Function Pair Implementation Added to the Tracker.cpp File STDMETHODIMP CTracker::get_Indent(long * pVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) // TODO: Add your implementation
code here return S_OK; } STDMETHODIMP CTracker::put_Indent(long newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) // TODO: Add your implementation code here return S_OK; } The actual implementation of the Indent property is very simple (see Listing 4.15). get_Indent returns the value currently stored in the member variable, and put_Indent stores the new value, after a little bit of error checking, in the member variable.
Listing 4.15 TRACKER.CPP--Indent Property Implementation STDMETHODIMP CTracker::get_Indent(long * pVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; // return the member variable *pVal = m_lIndent; // return the result return hResult; } STDMETHODIMP CTracker::put_Indent(long newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; // if the new value is a least 0 if(newVal >= 0) // assign the value to our member variable m_lIndent = newVal; // return the result return hResult; } Properties, like methods, also have a wide variety of implementation options, including parameterized and enumerated values. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating properties. You've added methods and properties to the server, but you haven't really dealt with the issue of error handling in their implementation. In some cases, simply returning success or failure is not enough information for the developer to understand that an error occurred and what caused it. You will communicate more error information through the use of OLE exceptions.
Generating OLE Exceptions While executing a method call or some other action, at times you will find it necessary to terminate the process due to some critical error that has occurred or is about to occur. For example, a method is called to write data to a file, but the method cannot open the file because there is not enough room on the hard disk to do so. You must halt further processing until the error can be resolved. An error of this kind is known as an exception. Any type of error can be treated as an exception; it depends upon the requirements of your application and how you choose to deal with the errors that may result. You must become familiar with two forms of exceptions when creating ActiveX components. The first is a C++ exception. A C++ exception is a language mechanism used to generate critical errors of the type described earlier and is confined to the application in which they are defined. The second is an OLE exception. OLE exceptions are
used to communicate the same kinds of errors externally to applications that are using a component. The difference between the two is that C++ exceptions are used internally to an application's implementation and OLE exceptions are used externally to other applications. The COM implementation of a method in a dual-interface server does not have the same kind of error management features that IDispatch interfaces have. To generate the proper error information, an application must use the IErrorInfo object provided by the oper- ating system. A server need support only the ISupportErrorInfo interface, which lets an automation controller know that it should look at the IErrorInfo object for more information when an error occurs. The first step is to add an enumeration of the types of errors that the server can generate to the IDL file (see Listing 4.16). This step has the effect of publishing the error constants to the user of the automation server. Unlike the ITracker interface, the enumeration can be added anywhere within the IDL file and still produce the proper C++ declaration in the ATLServer.h file. Remember to generate a new CLSID for the enumeration using the GUIDGEN.EXE program. See Chapter 2 for more information on how to use the GUIDGEN program.
Listing 4.16 ATLSERVER.IDL--Error Enumeration Added to the IDL File . . . coclass CTracker { [default] interface ITracker; }; typedef [uuid(2B2AF9C9-5452-11D0-BEDE-00400538977D), helpstring("Tracker Error Constants")] enum tagTrackerError { MFCSERVER_E_NO_UBOUND = 46080, MFCSERVER_E_NO_LBOUND = 46081, MFCSERVER_E_NO_ARRAYLOCK = 46082, MFCSERVER_E_NO_FILE = 46083, MFCSERVER_E_BAD_ARRAY_PARAMETER = 46084, MFCSERVER_E_INVALID_VALUE = 46085 }TRACKERERROR; }; Since ATL servers are dual-interface by default, you must implement all errors using OLE rich error information and not with C++ exceptions. The next step is to add the actual error-generating code (see Listing 4.17). ATL provides the helper function AtlReportError for generating rich error information. The function accepts four parameters: the CLSID of the server, an error message, the IID of the interface, and the error code. Error codes must be formatted error codes, as in the implementation, or a predefined OLE error codes; simply returning S_FALSE is not enough to generate an error. See the VC++ books online for more information regarding OLE error codes and their use.
Listing 4.17 TRACKER.CPP--Rich Error Information Added to Tracker Implementation STDMETHODIMP CTracker::OutputLines(VARIANT * varOutputArray, VARIANT varIndent, VARIANT_BOOL * RetVal) {
AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; *RetVal = VARIANT_TRUE; // if we have a file a if the variant contains a string array if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR)) { // lock the array so we can use it if(::SafeArrayLock(varOutputArray->parray) == S_OK) { LONG lLBound; // get the lower bound of the array if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK) { LONG lUBound; // get the number of elements in the array if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound) == S_OK) { CString cstrIndent; CTime oTimeStamp; BSTR bstrTemp; // if we have an indent parameter if(varIndent.vt != VT_I4) { // get a variant that we can use // for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something // useful - VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType (&varConvertedValue, (VARIANT *) &varIndent, 0, VT_I4)) // assign the value to our member variable m_lIndent = varConvertedValue.lVal; } else // assign the value to our member variable m_lIndent = varIndent.lVal; // if we have to indent the text for(long lIndentCount = 0; lIndentCount < m_lIndent; lIndentCount++) // add a tab to the string cstrIndent += _T("\t"); // for each of the elements in the array for(long lArrayCount = lLBound; lArrayCount < (lUBound + lLBound); lArrayCount++) { // update the time oTimeStamp = CTime::GetCurrentTime(); m_lHiResTime = timeGetTime(); // get the data from the array if(::SafeArrayGetElement (varOutputArray->parray, &lArrayCount, &bstrTemp) == S_OK) { // output the data fprintf(m_fileLog, _T("%s(%10ld)-%s%ls\n"), (LPCTSTR) oTimeStamp.Format ("%H:%M:%S"), m_lHiResTime - m_lLastHiResTime, (LPCTSTR) cstrIndent, bstrTemp); // store the last timer value m_lLastHiResTime = m_lHiResTime; // free the bstr
::SysFreeString(bstrTemp); } } } else { *RetVal = VARIANT_FALSE; // create the error message hResult = AtlReportError(CLSID_Tracker, "Unable to retrieve the upper bound dimension of the array.", IID_ITracker, MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_UBOUND)); } } else { *RetVal = VARIANT_FALSE; // create the error message hResult = AtlReportError(CLSID_Tracker, "Unable to retrieve the lower bound dimension of the array.", IID_ITracker, MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_LBOUND)); } // unlock the array we don't need it anymore ::SafeArrayUnlock(varOutputArray->parray); } else { *RetVal = VARIANT_FALSE; // create the error message hResult = AtlReportError(CLSID_Tracker, "Unable to lock the array memory.", IID_ITracker, MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_ARRAYLOCK)); } } else { *RetVal = VARIANT_FALSE; // if there wasn't a file if(!m_fileLog) // create the error message hResult = AtlReportError(CLSID_Tracker, "Invalid File Handle. File could not be opened for output.", IID_ITracker, MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE)); else // create the error message hResult = AtlReportError(CLSID_Tracker, "The first parameter must be a string array passed by reference.", IID_ITracker, MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_BAD_ARRAY_PARAMETER)); } // return the result
return hResult; } STDMETHODIMP CTracker::get_Indent(long * pVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; // return the member variable *pVal = m_lIndent; // return the result return hResult; } STDMETHODIMP CTracker::put_Indent(long newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; // if the new value is a least 0 if(newVal >= 0) // assign the value to our member variable m_lIndent = newVal; else { // create the error message hResult = AtlReportError(CLSID_Tracker, "Invalid value. Value must be 0 or greater.", IID_ITracker, MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_INVALID_VALUE)); } // return the result return hResult; } The use of C++ exceptions is still permitted with an ATL-implemented server. But the exception cannot cross application boundaries, which is the case in any application whether implemented in MFC, ATL, or some other framework.
Dual-Interface In Chapter 3, the basic MFC server is implemented as IDispatch only. You are required to add dual-interface support as an extra step. With the ATL, dual-interface support is built-in and implemented as a normal aspect of the server.
Generating Dual-Interface OLE Exceptions Again, as in Chapter 3, the basic MFC server is implemented as IDispatch only, and it is possible to throw standard C++ exceptions and have the basic MFC IDispatch support code translate the error into an OLE exception. When an MFC server is converted to dual-interface, you must implement the exception translation code yourself. For ATL, the server has been implemented as dual-interface from the start, and all error generation has been written as true OLE exceptions and does not require translation.
Server Instantiation Using C++ OLE is not the only method for creating and using Automation Servers. This chapter will show you how to create OLE servers using C++ syntax.
At times, you must create and use Automation Servers from within the application in which they are defined. Take, for example, a case where an application contains three servers, with only one being directly creatable by outside applications using OLE. The remaining two servers can be created by the exposed server using C++ and returned via a method call to another application, which then uses the server as though it was created via OLE. For an MFC server, the inclusion or exclusion of the macros DECLARE_OLECREATE and IMPLEMENT_OLECREATE determines whether a server is creatable by external applications. For ATL, it is a little simpler. All ATL applications contain a global variable called an ObjectMap for declaring all of the servers that can be created via OLE. The ObjectMap is declared in the main application file (see the file ATLServer.cpp) as a pair of macros: BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP() Each OLE server implemented within the application will have a single entry within the body of the ObjectMap macro, thus identifying the server as an exposed OLE server. The OBJECT_ENTRY macro defines the CLSID and the C++ class of the server that can be created. BEGIN_OBJECT_MAP(ObjectMap) OBJECT_ENTRY(CLSID_Tracker, CTracker) END_OBJECT_MAP() To prevent an application from being exposed as an Automation Server, you just remove or comment out the entry in the object map. When adding additional servers to an application, you must add an entry for each new server to the ObjectMap macro. All ATL servers contain a static function CreateInstance, which is used to instantiate instances of themselves. You must use only the CreateInstance function to instantiate a server since it is implemented by the class factory of the server and will manage the server instantiation correctly. This is very critical in the cases where the server is shared among two or more applications, as you will see in the following sections. The first step is to declare the pointer to which you will store the reference of the object when it is created. Because ATL servers are implemented using templates, it may seem a little strange to declare the reference this way. However, this makes sense when you see the architecture of ATL, which is documented fully in the VC++ books online.
CComObject * opTracker; The next step is to instantiate the server and store the reference of the new object. Remember to check the return value of the function and the pointer to ensure that the object was instantiated successfully.
CComObject::CreateInstance(&opTracker); Once a server is instantiated this way, you can use it like any other C++ class or OLE server. You can use QueryInterface to retrieve IDispatch or custom interface pointers that can be passed to other applications. Refer to Chapter 3 for more information regarding the instantiation and use of OLE servers with C++. So far, you've only looked at how to create individual instances of objects. In the following section, you will learn how to share objects.
Shared Servers
OLE defines a facility for sharing objects called the Running Object Table. Essentially, a shareable object will publish its CLSID and an IUnknown reference to itself in the Running Object Table. Any application that so desires can ask for the running instance of the object rather than create a new instance. Applications that may need to work with a single running instance of an application may find it more useful to use shared objects than to create multiple copies. The Tracker object is a perfect candidate for this kind of functionality. Multiple applications could use the same Tracker object to log information, thus saving on memory. Unfortunately, the way ATL is implemented prevents you from adding shared object support without actually creating new ATL template classes. This limitation occurs because of dependence on the Release function implementation to revoke the object from the Running Object Table, which you cannot override directly in the base ATL classes. Listing 4.18 shows the support code that has been added to the StdAfx.h file to support shared objects. The new classes and macros are based on the original ATL code and have been extended to register the server in the Running Object Table. The code will also remove the server from the Running Object Table when the reference count reaches 1. The only real change made to the original ATL code is that a new class CComObjectShared is added with an extra template parameter of the CLSID of the server. The remaining changes to the code and macros are to reflect the use of the new class versus its original implementation CComObject. Do note that the shared server implementation is simple and does not support aggregatable objects. But that is not to say it cannot be implemented; it just wasn't done for this sample. The constructor of the CComObjectShared class adds the IUnknown reference to the Running Object Table and stores the ID in a member variable to be used later when revoking the server. The Release function is implemented the same as the MFC sample in that the Release implementation revokes the server from the Running Object Table. The code must also protect the Release call by bumping up the reference count of the server and clearing the member variable to prevent recursion.
Listing 4.18 STDAFX.H--Shared Object Support Classes and Macros // ****** ATL 2.0 version - Added by Jerry Anderson for shared object support // ** #define DECLARE_NOT_AGGREGATABLE_SHARED(cBase, clsid) public:\ typedef CComCreator2< CComCreator< CComObjectShared >, CComFailCreator > _CreatorClass; // if this object was registered and the refcount is 1 (which is from the "RegisterActiveObject") // then revoke the registration so the object can be destroyed properly - The AddRef/Release pair // is to protect the destruction and prevent the object from being deleted before we are out of this call // since the RevokeActiveObject is going to call "Release" also and the refcount would be 0 if we didn't AddRef #define RELEASE_AND_DESTROY_SHARED() \ InternalRelease(); \ if(dwRegister && m_dwRef == 1) \ { InternalAddRef(); DWORD dwtRegID = dwRegister; dwRegister = 0; ::RevokeActiveObject(dwtRegID, NULL); InternalRelease(); } \
if(m_dwRef == 0) \ { delete this; return 0; } \ return m_dwRef //Base is the user's class that derives from CComObjectRoot and whatever //interfaces the user wants to support on the object template class CComObjectShared : public cBase { public: // this is here to prevent an ASSERT when executing "InternalFinalConstructRelease()" DECLARE_PROTECT_FINAL_CONSTRUCT(); ULONG dwRegister; typedef cBase _BaseClass; CComObjectShared(void* = NULL) { // protect the construction this->InternalAddRef(); // lock down the application so it does not fall out from under us _Module.Lock(); // clear the member dwRegister = NULL; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // QI for the IUnknown if(_InternalQueryInterface(IID_IUnknown, (void **) &pIUnknown) == S_OK) { // register the clsid as an active object so other applications will get the same object if(::RegisterActiveObject(pIUnknown, *clsid, ACTIVEOBJECT_STRONG, &dwRegister) != S_OK) // clear the member dwRegister = NULL; // release the IUnknown pIUnknown->Release(); } // protect the construction this->InternalRelease(); } virtual ~CComObjectShared() { // Set refcount to 1 to protect destruction m_dwRef = 1L; FinalRelease(); _Module.Unlock(); } //If InternalAddRef or InteralRelease is undefined then your class //doesn't derive from CComObjectRoot STDMETHOD_(ULONG, AddRef)() { // release the IUnknown reference return InternalAddRef(); } STDMETHOD_(ULONG, Release)() { RELEASE_AND_DESTROY_SHARED();
} //if _InternalQueryInterface is undefined then you forgot BEGIN_COM_MAP STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {return _InternalQueryInterface(iid, ppvObject);} static HRESULT WINAPI CreateInstance(CComObjectShared**pp); }; // needed for ATL version 1.1 // template // CComObjectShared* CComObjectShared::pSharedObject = NULL; template HRESULT WINAPI CComObjectShared::CreateInstance(CComObjectShared** pp) { _ASSERTE(pp != NULL); HRESULT hRes = E_OUTOFMEMORY; CComObjectShared* p = NULL; ATLTRY((p = new CComObjectShared())) if (p != NULL) { p->SetVoid(NULL); p->InternalFinalConstructAddRef(); hRes = p->FinalConstruct(); // this line differs from the original code // for some reason the reference counts // for the object are not correct when created this way p->InternalAddRef(); p->InternalFinalConstructRelease(); if (hRes != S_OK) { delete p; p = NULL; } } *pp = p; return hRes; } // ** // ****** ATL 2.0 version - Added by Jerry Anderson for shared object support As the server developer, the only thing you must do is add the macro DECLARE_NOT_AGGREGATABLE_SHARED(...) to the ATL server class. Listing 4.19 shows the change that was made to the CTracker sample to enable shared server support.
Listing 4.19 TRACKER.H--Shared Server Support Added to Ctracker Class . . . DECLARE_REGISTRY_RESOURCEID(IDR_TRACKER) BEGIN_COM_MAP(CTracker) COM_INTERFACE_ENTRY(ITracker) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(ISupportErrorInfo) END_COM_MAP() DECLARE_NOT_AGGREGATABLE_SHARED(CTracker, &CLSID_Tracker) // ISupportsErrorInfo STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker public:. . .
During the lifetime of the server, you can get the same instance of the server and use it from multiple applications. In VB, getting the running instance of a server is done with the GetObject call and in VC++ with the GetActiveObject function. After the pointer to the server is retrieved, the server can be used as though it was created through normal OLE mechanisms. This method of sharing objects is fine but requires that the application using the server take an active role in deciding to use the shared object versus creating its own instance of the object. Another approach is to supply the instance of a running server to an application that calls CreateObject rather than GetObject. This approach is known as a single instance server.
Single Instance Servers To support single instance servers, you must perform all of the steps described in the section entitled "Shared Servers," earlier in this chapter, from within the ClassFactory of the server, not from within the implementation of the server itself. By implementing the object sharing code within the class factory, you are able to control the number of instances of the server without having to rely on the user of the server to program specifically for those cases. As is the case with the shared server support, we have created a new set of classes and macros for creating a server consisting of a single instance (see Listing 4.20). The classes are based on the existing ATL classes and differ slightly from the original implementation. In this case, two new classes were created: CComCreatorSingle and CComObjectSingle. CComCreatorSingle is the class that is responsible for creating the server--if one has not already been created-or retrieving the existing server and returning it instead of creating a new instance. CComObjectSingle is also responsible for registering the server in the Running Object Table. As with the shared server implementation, CComObjectSingle removes itself from the Running Object Table, thereby protecting itself from recursive entry into the Release function. The CComObjectSingle also supports a CreateInstance function, which is used to create copies of the server using C++ syntax. CComObjectSingle::CreateInstance must also contain support to create a new server if one is not already running or use the existing server if available.
Listing 4.20 STDAFX.G--Single Instance Server Support Added to StdAfx.h // ****** ATL 2.0 version - Added by Jerry Anderson for single instance object support // ** #define DECLARE_NOT_AGGREGATABLE_SINGLE(cBase, clsid, iBase, iid) public: \ typedef CComCreator2< CComCreatorSingle< CComObjectSingle, clsid>, CComFailCreator > CreatorClass; // if this object was registered and the refcount is 1 (which is from the // "RegisterActiveObject") // then revoke the registration so the object can be destroyed properly - The // AddRef/Release pair // is to protect the destruction and prevent the object from being deleted before we are // out of this call // since the RevokeActiveObject is going to call "Release" also and the refcount would // be 0 if we didn't AddRef #define
RELEASE_AND_DESTROY_SINGLE() \ InternalRelease(); \ if(dwRegister && m_dwRef == 1) \ { InternalAddRef(); DWORD dwtRegID = dwRegister; dwRegister = 0; ::RevokeActiveObject(dwtRegID, NULL); InternalRelease(); } \ if(m_dwRef == 0) \ { delete this; return 0; } \ return m_dwRef // Base is the user's class that derives from CComObjectRoot and whatever // interfaces the user wants to support on the object template class CComObjectSingle : public cBase { public: // this is here to prevent an ASSERT when executing "InternalFinalConstructRelease()" DECLARE_PROTECT_FINAL_CONSTRUCT(); ULONG dwRegister; typedef cBase _BaseClass; CComObjectSingle(void* = NULL) { // protect the construction this->InternalAddRef(); // lock down the application so it does not fall out from under us _Module.Lock(); // clear the member dwRegister = NULL; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // se if the object is already running ::GetActiveObject(*clsid, NULL, &pIUnknown); // if we didn't get a reference to a running object if(!pIUnknown) { // QI for the IUnknown if(_InternalQueryInterface(IID_IUnknown, (void **) &pIUnknown) == S_OK) { // register the clsid as an active object so other applications // will get the same object ::RegisterActiveObject(pIUnknown, *clsid, ACTIVEOBJECT_STRONG, &dwRegister); // release the IUnknown pIUnknown->Release(); } // clear the reference just to be safe pIUnknown = NULL; } else // release the IUnknown pIUnknown->Release(); // protect the construction this->InternalRelease(); } virtual ~CComObjectSingle() {
// Set refcount to 1 to protect destruction m_dwRef = 1L; FinalRelease(); _Module.Unlock(); } //If InternalAddRef or InteralRelease is undefined then your class //doesn't derive from CComObjectRoot STDMETHOD_(ULONG, AddRef)() { // release the IUnknown reference return InternalAddRef(); } STDMETHOD_(ULONG, Release)() { RELEASE_AND_DESTROY_SINGLE(); } //if _InternalQueryInterface is undefined then you forgot BEGIN_COM_MAP STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {return _InternalQueryInterface(iid, ppvObject);} static HRESULT WINAPI CreateInstance(CComObjectSingle** pp); }; // needed for ATL version 1.1 // template // CComObjectSingle* CComObjectSingle::pSingleObject = NULL; template HRESULT WINAPI CComObjectSingle::CreateInstance(CComObjectSingle** pp) { _ASSERTE(pp != NULL); HRESULT hRes = E_OUTOFMEMORY; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // se if the object is already running ::GetActiveObject(*clsid, NULL, &pIUnknown); // if we didn't get a reference to a running object if(!pIUnknown) { CComObjectSingle* p = NULL; ATLTRY((p = new CComObjectSingle())) if (p != NULL) { p->SetVoid(NULL); p->InternalFinalConstructAddRef(); hRes = p->FinalConstruct(); // this line differs from the original code // for some reason the reference counts // for the object are not correct when created this way p->InternalAddRef(); p->InternalFinalConstructRelease(); if (hRes != S_OK) {
delete p; p = NULL; } } *pp = p; } else { // get a pointer iBase * piBase = NULL; // QI for the interface pIUnknown->QueryInterface(*iid, (LPVOID*) &piBase); // cast the interface pointer to the class *pp = (CComObjectSingle*) piBase; // release the IUnknown reference pIUnknown->Release(); } return hRes; } template class CComCreatorSingle { public: static HRESULT PASCAL CreateInstance(void* pv, REFIID riid, LPVOID* ppv) { _ASSERTE(*ppv == NULL); HRESULT hRes = E_OUTOFMEMORY; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // se if the object is already running ::GetActiveObject(*clsid, NULL, &pIUnknown); // if we didn't get a reference to a running object if(!pIUnknown) { T1* p = NULL; ATLTRY(p = new T1(pv)) if (p != NULL) { p->SetVoid(pv); p->InternalFinalConstructAddRef(); hRes = p->FinalConstruct(); p->InternalFinalConstructRelease(); if (hRes == S_OK) hRes = p->QueryInterface(riid, ppv); if (hRes != S_OK) delete p; } } else { // get the IID that was requested hRes = pIUnknown->QueryInterface(riid, ppv); // release the IUnknown reference pIUnknown->Release(); }
return hRes; } }; // ** // ****** ATL 2.0 version - Added by Jerry Anderson for single instance object support As with shared server support, you must add a new macro to your class definition to enable your server for single instance support. Add the macro DECLARE_NOT_AGGREGATABLE_SINGLE to the CTracker class definition supplying the class name, CLSID, interface name, and IID of your server. Listing 4.21 shows the CTracker implementation of single instance server support.
Listing 4.21 TRACKER.H--Single Instance Server Support Added to CTracker Class . . . DECLARE_REGISTRY_RESOURCEID(IDR_TRACKER) BEGIN_COM_MAP(CTracker) COM_INTERFACE_ENTRY(ITracker) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(ISupportErrorInfo) END_COM_MAP() // DECLARE_NOT_AGGREGATABLE_SHARED(CTracker, &CLSID_Tracker) DECLARE_NOT_AGGREGATABLE_SINGLE(CTracker, &CLSID_Tracker, ITracker, &IID_ITracker) // ISupportsErrorInfo STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker . . . Shared server support is very straightforward to implement and use and adds a level of functionality not normally available to standard server implementations.
From Here... In this chapter, you created a basic implementation of an Automation Server. You also expanded upon the basic framework provided by ATL to create new and interesting features within your implementation. ATL provides a clean and easy way to implement Automation Servers. Combined with MFC or STL (Standard Template Library), ATL is a powerful platform for creating ActiveX components. ATL has the added benefit of being a product supported by Microsoft, which is not the case with the BaseCtl framework. Like MFC, ATL servers can benefit from the addition of User Interface and Events. The creation of services and remote servers also makes the prospect of implementing ActiveX servers enticing. Automation Servers provide a flexible way to create lightweight ActiveX components for use by your applications. The support of both IDispatch interfaces and custom interfaces (a dual-interface server) also gives the user of the server a lot of flexibility in terms of implementation styles and methods. The next chapter looks at creating an Automation Server using the BaseCtl framework.
Chapter 5 Creating ActiveX Automation Servers Using BaseCtl ●
Creating ActiveX Automation Servers Using BaseCtl ❍ Creating the Basic Project ❍ Registry ■ Listing 5.1 TRACKER.H--Basic Registration Information in the DEFINE_AUTOMATIONOBJECT Structure ❍ Sample Server Support Code ■ Listing 5.2 TRACKER.H--Sample Server Support Code Added to the Header File ■ Listing 5.3 TRACKER.CPP--Sample Server Support Code Added to the Source File ❍ Adding Methods ■ Listing 5.4 BCFSERVER.ODL--OutputLines Method ODL Declaration ■ Listing 5.5 TRACKER.H--OutputLines Function Prototype Added to the Class Definition ■ Listing 5.6 TRACKER.CPP--Member Initialization in the Constructor ■ Listing 5.7 TRACKER.CPP--OutputLines Function Implementation Added to the Source File Tracker.cpp ❍ Adding Properties ■ Listing 5.8 BCFSERVER.ODL--Indent Property Added to the ODL ■ Listing 5.9 TRACKER.H--Indent Property Function Prototypes Added to the Class Definition ■ Listing 5.10 TRACKER.CPP--Indent Property Implementation ❍ Generating OLE Exceptions ■ Listing 5.11 BCFSERVER.ODL--Error Enumeration in the ODL File ■ Listing 5.12 TRACKER.CPP--Rich Error Information Added to Tracker Implementation ❍ Dual-Interface ❍ Generating Dual-Interface OLE Exceptions ❍ Server Instantiation Using C++ ■ Listing 5.13 Example--Creating an OLE Server Using C++ ❍ Shared Servers ■ Listing 5.14 TRACKER.CPP--Create Function Implementation of Shared Object Support ■ Listing 5.15 TRACKER.H--DECLARE_STANDARD_UNKNOWN_SHARED Macro Implementation
Listing 5.16 TRACKER.H--New Macro Single Instance Servers ■ Listing 5.17 TRACKER.CPP--Create Function Implementation Updated to Support Single Instance Servers User Interface and Events From Here... ■
❍
❍ ❍
Creating ActiveX Automation Servers Using BaseCtl ●
●
●
●
●
Server registration support Like ATL, the basic BaseCtl framework allows registration and unregistration support without additional code modification. Adding methods and properties The single greatest drawback in using BaseCtl is its lack of integration with the VC++ IDE. Properties provide a uniform mechanism for getting and setting the state of a server's data variables. OLE exceptions Unlike MFC, BaseCtl does not support OLE exceptions through a C++ exception class; BaseCtl takes advantage of OLE error reporting mechanisms. Dual-interface BaseCtl servers implement dual-interface as a standard feature of the application. Shared servers and single instance servers Adding shared server support to the BaseCtl framework gives insights into how the BaseCtl class factory is implemented.
The history of the BaseCtl framework has its roots in the Visual Basic (VB) group where it was first developed. The single most unique thing about the BaseCtl Framework is its lack of dependence on MFC, which was viewed by the VB team as a burden to creating small and fast OCXs. Several versions of the BaseCtl framework are floating around. The basic version is the one that ships with the ActiveX SDK, consisting of a number of source files and several samples. A more thorough version, consisting of more samples and even an AppWizard written in VB, has been available to the members of the VB 5 beta testing group for some time. The BaseCtl framework is intended merely as a sample application and does not have the same support
and backing of Microsoft as do its other development products. In addition to creating ActiveX Controls (see Chapter 8 for more information), the BaseCtl framework can be used to create ActiveX Automation Servers, which are the focus of this chapter. Like ATL, the BaseCtl is not integrated with the VC++ IDE and requires that all code be entered and modified by hand. In this chapter, you will create a simple in-process Automation Server. As with Chapters 3 and 4, your sample server implementation will be used to log string data to a file.
Creating the Basic Project Unfortunately, the BaseCtl framework that ships in the ActiveX SDK does not have an AppWizard like its MFC and ATL counterparts. We have provided a sample project based on the BaseCtl samples from which you can create new projects. The sample application that we have created is a bare-bones project similar to the kind that would be generated by an AppWizard. We've included our own sample application because the samples included with the BaseCtl framework are already implemented with specific features and are intended to demonstrate different aspects of BaseCtl development, whereas our sample is meant as a starting point for your own component development. To create a new project, you first need to define a new directory into which you can copy the sample files; in this case, call the directory BCFServer. Copy all of the files from the BCFBasicServer directory into your new directory, BCFServer. Rename all of the files in the BCFServer directory as described in Table 5.1. The files dispids.h, dwinvers.h, guids.cpp, guids.h, localobj.h, and resource.h will remain as they are. Table 5.1 New Filenames Old Filename
New Filename
BasicAutoObj.h
Tracker.h
BasicAutoObj.cpp
Tracker.cpp
BCFBasicServer.h
BCFServer.h
BCFBasicServer.cpp
BCFServer.cpp
BCFBasicServer.def
BCFServer.def
BCFBasicServer.mak
BCFServer.mak
BCFBasicServer.odl
BCFServer.odl
BCFBasicServer.rc
BCFServer.rc
Open the VC++ IDE, and from the File menu select the Find In Files menu item. Look for BCFBasicServer in all of the files in the BCFServer directory. In all of the files that contain the string BCFBasicServer, replace the text with BCFServer. Make sure to do the text replacement on a case-sensitive basis. Repeat the Find In Files search to ensure that you did not miss any files or
BCFBasicServer entries. Next search for the string BasicAutoObj and replace all of the entries with Tracker. Do the same for the string BasicAuto. Remember all text should be replaced on a case-sensitive basis and should be done for all of the files in the new project directory. The last step is to generate new CLSID that are unique to the project. Use the GUIDGEN.EXE to create the new CLSID, and add them to the ODL file. After all of the files have been changed, from the File menu, select the Open Workspace menu item. In the Open Workspace dialog, open the BCFServer.mak file. The VC++ IDE will automatically create the file BCFServer.mdp file, which should be used from this point on when opening the project file.
NOTE: At the time of the writing of this book, VC++ 5.0 was still in beta. The file extensions .mak and .mdp have been replaced with the extensions .dsp and .dsw, respectively. Using the older file extensions will in no way affect the sample project or your development.
The last step is to ensure that the location of the BaseCtl include files directory and the path of the BaseCtl libraries are correct. The BCFBasicServer sample assumes that the include directory is established in the Directory tab of the Options dialog, which can be accessed via the Tools menu and the Options menu item. The link settings of the basic project is where the path of the BaseCtl libraries is defined.
Linking Got You Down? When building your projects utilizing the BaseCtl framework, make sure that you use exactly the same compile settings as those used to create the library. It is important to check all of the settings to ensure that they match exactly. If the compile options do not match, the linker will be unable to create your project and will generate a number of linker errors.
At this point, you can compile the project and even register it in the system registry. However, you can do very little with it since it does not contain any methods or properties. Like all ActiveX/OLE components, ActiveX Servers are required to support registration and unregistration if they are to be used by other applications.
Registry
The basic registration and unregistration support for the server is already implemented by BaseCtl. Remember that the MFC implementation only allows for registering the server. Registration support is handled completely by the BaseCtl framework and is hidden from the developer. The registration information is part of the DEFINE_AUTOMATIONOBJECT structure (see Listing 5.1). See the BaseCtl documentation and samples for information about the structure and its content.
Listing 5.1 TRACKER.H--Basic Registration Information in the DEFINE_AUTOMATIONOBJECT Structure // TODO: modify anything appropriate in this structure, such as the helpfile // name, the version number, etc. // DEFINE_AUTOMATIONOBJECT(Tracker, &CLSID_Tracker, "Tracker", CTracker::Create, 1, &IID_ITracker,
"Tracker.Hlp");
Sample Server Support Code Since the server is used to output data to a file, you need to add some support code to the application before adding its methods and properties. Listing 5.2 shows the additions that are made to the class header file. First you added two new include files that are needed to support the file and timer functions that the server uses. A set of member variables is added for storing the file handle and timer information that will be used throughout the server implementation.
Listing 5.2 TRACKER.H--Sample Server Support Code Added to the Header File // #ifndef _TRACKER_H_ #include "AutoObj.H" #include "BCFServerInterfaces.H" // needed for FILE services
#include // needed for the high resolution timer services #include class CTracker : public ITracker, public CAutomationObject, ISupportErrorInfo { public: . . . protected: virtual HRESULT InternalQueryInterface(REFIID riid, void **ppvObjOut); private: // member variables that nobody else gets to look at. // TODO: add your member variables and private functions here. protected: FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime;
}; In the sample implementation, the constructor and destructor implementations are updated to open and close the log file (see Listing 5.3). First a high resolution timer is created and its current value is stored in the member variables. The timer is useful for determining the number of milliseconds that have passed since the last method call was made. This is great for tracking the performance of a particular action or set of actions. You then get the current date and create a filename with the format YYYYMMDD.tracklog. After successfully opening the file, you output some startup data to the file and exit the constructor. The destructor does the exact opposite of the constructor. If you have a valid file handle, you write some closing information to the file and close it. Next you terminate the timer.
Listing 5.3 TRACKER.CPP--Sample Server Support Code Added to the Source File #pragma warning(disable:4355) // using `this' in constructor CTracker::CTracker (
IUnknown *pUnkOuter ) : CAutomationObject(pUnkOuter, OBJECT_TYPE_OBJTRACKER, (void *)this) { // setup our timer resolution m_lTimeBegin = timeBeginPeriod(1); m_lHiResTime = m_lLastHiResTime = timeGetTime(); SYSTEMTIME sTime; // get the current date and time ::GetLocalTime(&sTime); TCHAR tstrFileName[18]; // format the file name based on the current date sprintf(&tstrFileName[0], "%04d%02d%02d.tracklog", sTime.wYear, sTime.wMonth, sTime.wDay); // open a file m_fileLog = fopen(&tstrFileName[0], "a"); // if we have a file handle if(m_fileLog) { // output some starting information fprintf(m_fileLog, "************************\n"); fprintf(m_fileLog, "Start %02d/%02d/%04d - %02d:%02d:%02d\n", sTime.wMonth, sTime.wDay, sTime.wYear, sTime.wHour, sTime.wMinute, sTime.wSecond); fprintf(m_fileLog, "\n"); } } #pragma warning(default:4355) // using `this' in constructor //=-------------------------------------------------------------------------= // CTracker::CTracker //=-------------------------------------------------------------------------= // "We all labour against our own cure, for death is the cure of all diseases" // - Sir Thomas Browne (1605 - 82) // // Notes: // CTracker::~CTracker () {
// if we have a file handle if(m_fileLog) { SYSTEMTIME sTime; // get the current date and time ::GetLocalTime(&sTime); // output some closing information fprintf(m_fileLog, "\n"); fprintf(m_fileLog, "End %02d/%02d/%04d - %02d:%02d:%02d\n", sTime.wMonth, sTime.wDay, sTime.wYear, sTime.wHour, sTime.wMinute, sTime.wSecond); fprintf(m_fileLog, "************************\n"); // close the file fclose(m_fileLog); }
} The last thing you need to do is update the build settings for the project. Since the sample implementation is using some timer functions defined in mmsystem.h, you need to link with the appropriate library file that contains their implementation. Under the Project menu, select the Settings menu item. In the Project Settings dialog select the Link tab, and add the file winmm.lib to the Object/library modules edit field. Click OK to close the dialog. The basic support code needed for the sample implementation has been added. The server will open a file in its constructor and leave the file open during its entire lifetime. When the server is destroyed, the destructor will be called, and the file will be closed. The next step is to make the sample more meaningful by adding methods and properties, which are used to output data to the open file.
Adding Methods An automation method consists of zero to n parameters and may or may not have a return value. The term method is synonymous with function or subroutine, depending on the particular language you are familiar with. Since your server is IDispatch-based, you are limited to a specific set of data types. Only those data types that are valid VARIANT data types can be passed or returned via a method. The rules for declaring parameters and how they are used is very much like C++ and VB. Methods can pass parameters by value or by reference and may also declare them as optional, meaning that the parameter does not have to be supplied.
When passing a parameter by value, a copy of the data is sent to the method. When passing by reference, the address of the parameter is passed, allowing the method to change the data. Optional parameters are handled a little differently than C++, however, because you can't specify a default value in the traditional C++ sense. Optional parameters must be passed as VARIANT data types and not the actual data type they represent. For developers using VB to access a method with optional parameters, VB will supply the parameter for you if one has not been provided. With C++, you are still required to supply a VARIANT parameter, even though it may not contain any data. As we stated at the beginning of the chapter, the sample automation server will be used to log strings of data to a file. The server will define the method OutputLines, which is used by the user of the server to supply the string data that is written to the file. The method will accept an array of strings and an optional indentation parameter and will output the strings to the file. The indentation parameter is used to offset the strings by n number of tab characters to provide simple, yet effective, formatting to the data as it is output to the file. All work involving methods and properties in BaseCtl (and also ATL) starts with the ODL (or IDL) file. Methods and properties are declared here, first, and then implemented in the server. BaseCtl Automation Servers are dual-interface by default, so you must declare all of the methods and properties so as to conform to standard dual-interface rules. Refer to Chapters 3 and 4 for more information on dual-interface. Listing 5.4 shows the declaration of the OutputLines method. OutputLines is defined as having two parameters: varOutputArray, as a VARIANT passed by reference that will contain a string array of data to output to the file, and varIndent, as a VARIANT passed by value, which is also an optional parameter indicating the amount of indentation when writing the string data to the file. The third parameter is actually the return type of the method and is defined as a Boolean. See Chapter 3 regarding the use of Boolean data types and the differences between VB and VC++. Due to data type restrictions imposed by OLE Automation, you cannot pass arrays as parameters of methods. You can, however, pass VARIANT data types that can contain arrays, thus the reason for defining varOutputArray as a Variant. You are also required to pass varOutputArray by reference because the array stored in the VARIANT does not get copied over when it is passed by value. Optional parameters must fall at the end of the parameter list and must be of type VARIANT. varIndent is an optional parameter that indents your text output as an added formatting feature (see Listing 5.4).
Listing 5.4 BCFSERVER.ODL--OutputLines Method ODL Declaration . . . [ uuid(5ea29be0-5a82-11d0-bcbc-0020afd6738c), helpstring("Tracker Object"), hidden, dual, odl ] interface ITracker : IDispatch { // properties // // methods [id(2)] HRESULT OutputLines([in] VARIANT * varOutputArray, [in, optional] VARIANT varIndent, [out, retval] boolean * RetVal); }; // coclass for CTracker objects //
. . . When the ODL file is compiled into a type library, the compiler will create a header file that contains all of the interface and CLSID declarations in C++ format. It is from this file that you will copy all of the function prototypes that are needed in your server implementation for defining the methods and properties that it contains. From the file BCFServerInterfaces.h, copy the OutputLines function prototype, and paste it into the class definition of the server (see Listing 5.5). Remember to remove the PURE keyword from the function prototype. For the purposes of the sample implementation, you also need to add the m_lIndent member variable, which is used in the OutputLines method implementation, and later as a property of the server.
Listing 5.5 TRACKER.H--OutputLines Function Prototype Added to the Class Definition . . . CTracker(IUnknown *); virtual ~CTracker();
// ITracker methods STDMETHOD(OutputLines)(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal); // creation method // static IUnknown *Create(IUnknown *); protected: virtual HRESULT InternalQueryInterface(REFIID riid, void **ppvObjOut); private: // member variables that nobody else gets to look at. // TODO: add your member variables and private functions here. protected: FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime; long m_lIndent; };
. . . Before adding the OutputLines implementation, it is necessary to update the constructor to initialize the m_lIndent member variable to a valid state (see Listing 5.6).
Listing 5.6 TRACKER.CPP--Member Initialization in the Constructor . . . // output some starting information fprintf(m_fileLog, "************************\n"); fprintf(m_fileLog, "Start %02d/%02d/%04d - %02d:%02d:%02d\n", sTime.wMonth, sTime.wDay, sTime.wYear, sTime.wHour, sTime.wMinute, sTime.wSecond); fprintf(m_fileLog, "\n"); } m_lIndent = 0; }
#pragma warning(default:4355) // using `this' in constructor Next you add the OutputLines implementation to the source file as in Listing 5.7. The implementation varies very little from the ATL sample. As with the MFC and ATL implementations, the BaseCtl version checks the array parameter to ensure its validity and, if so, outputs the data to the file, indenting the text if appropriate. See Chapter 3 for more information regarding the other implementation details. For now, the implementation returns VARIANT_FALSE in the cases where an error has occurred. Later in this chapter, you will learn how to create rich error information.
Listing 5.7 TRACKER.CPP--OutputLines Function Implementation Added to the Source File Tracker.cpp STDMETHODIMP CTracker::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal) { HRESULT hResult = S_OK; *RetVal = VARIANT_TRUE; // if we have a file and if the variant contains a string array if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR)) { // lock the array so we can use it if(::SafeArrayLock(varOutputArray->parray) == S_OK) { LONG lLBound; // get the lower bound of the array if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK) { LONG lUBound; // get the number of elements in the array if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound) == S_OK) { SYSTEMTIME sTime; BSTR bstrTemp; // if we have an indent parameter if(varIndent.vt != VT_I4) {
// get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful // - VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType(&varConvertedValue, (VARIANT *) &varIndent, 0, VT_I4)) // assign the value to our member variable m_lIndent = varConvertedValue.lVal; } else // assign the value to our member variable m_lIndent = varIndent.lVal; // for each of the elements in the array for(long lArrayCount = lLBound; lArrayCount < (lUBound + lLBound); lArrayCount++) { // get the current date and time ::GetLocalTime(&sTime); m_lHiResTime = timeGetTime(); // get the data from the array if(::SafeArrayGetElement(varOutputArray->parray, &lArrayCount, &bstrTemp) == S_OK) { // output the data fprintf(m_fileLog, "%02d:%02d:%02d(%10ld)-", sTime.wHour, sTime.wMinute, sTime.wSecond, m_lHiResTime m_lLastHiResTime); // if we have to indent the text for(long lIndentCount = 0; lIndentCount < m_lIndent; lIndentCount++) // add a tab to the string fprintf(m_fileLog, "\t"); // output the data fprintf(m_fileLog, "%ls\n", bstrTemp); // store the last timer value m_lLastHiResTime = m_lHiResTime; // free the bstr ::SysFreeString(bstrTemp);
} } } else *RetVal = VARIANT_FALSE; } else *RetVal = VARIANT_FALSE; // unlock the array we don't need it anymore ::SafeArrayUnlock(varOutputArray->parray);
} else
*RetVal = VARIANT_FALSE; } else *RetVal = VARIANT_FALSE; // return the result return hResult;
} Now that you have added a method, you will learn how to implement its counterpart, the Property.
Adding Properties A property can be thought of as an exposed variable that is defined in the automation server. Properties are useful for setting and retrieving information about the state of the server. The m_lIndent member variable that you added to the class definition is a perfect candidate to be exposed as a property. As with methods, properties are also added to a server via the ODL file. Listing 5.8 shows the ODL declaration for the Indent property. As stated in Chapters 3 and 4, properties are implemented as a pair of methods, one to get the value and the other to set the value. Since the server is dual-interface by default, the methods must also conform to dual-interface declaration rules.
Listing 5.8 BCFSERVER.ODL--Indent Property Added to the ODL
. . . interface ITracker : IDispatch { // properties [id(1), propget] HRESULT Indent([out, retval] long * Value); [id(1), propput] HRESULT Indent([in] long Value); // methods [id(2)] HRESULT OutputLines([in] VARIANT * varOutputArray, [in, optional] VARIANT varIndent, [out, retval] boolean * RetVal); };
. . . Compile the ODL file, and copy the function prototypes from the BCFServerInterfaces.h file to the Tracker class definition (see Listing 5.9). Don't forget to remove the PURE keyword from the new function declarations.
Listing 5.9 TRACKER.H--Indent Property Function Prototypes Added to the Class Definition . . . CTracker(IUnknown *); virtual ~CTracker(); // ITracker methods STDMETHOD(get_Indent)(long FAR* Value); STDMETHOD(put_Indent)(long Value); STDMETHOD(OutputLines)(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal); // creation method // static IUnknown *Create(IUnknown *);
. . . The actual implementation of the Indent property is very simple (see Listing 5.10). Get_Indent
returns the value currently stored in the member variable, and Put_Indent stores the new value, after a little bit of error checking, in the member variable.
Listing 5.10 TRACKER.CPP--Indent Property Implementation STDMETHODIMP CTracker::get_Indent(long FAR *Value) { HRESULT hResult = S_OK; // return the member variable *Value = m_lIndent; // return the result return hResult; } STDMETHODIMP CTracker::put_Indent(long Value) { HRESULT hResult = S_OK; // if the new value is a least 0 if(Value >= 0) // assign the value to our member variable m_lIndent = Value; // return the result return hResult;
} Properties, like methods, also have a wide variety of implementation options, including parameterized and enumerated values. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating properties. You've added methods and properties to the server, but you haven't really dealt with the issue of error handling in their implementation. In some cases, simply returning success or failure is not enough information for the developer to understand that an error occurred and what caused it. You will communicate more error information through the use of OLE exceptions.
Generating OLE Exceptions While executing a method call or some other action, it is necessary at times to terminate the process due to some critical error that has occurred or is about to occur. For example, a method is called to write data to a file, but the method cannot open the file because there is not enough room on the hard
disk to do so. It is necessary to halt further processing until the error can be resolved. This is known as an exception. Any type of error can be treated as an exception; it depends on the requirements of your application and how you choose to deal with the errors that may result. You must become familiar with two forms of exceptions when creating ActiveX components. The first is a C++ exception. A C++ exception is a language mechanism used to create critical errors of the type described earlier and that are confined to the application in which they are defined. The second is an OLE exception. OLE exceptions are used to communicate the same kinds of errors externally to applications that are using a component. The difference between the two is that C++ exceptions are used internally to an application's implementation and OLE exceptions are used externally to communicate errors to other applications. The COM implementation of a method in a Dual-Interface Server does not have the same kind of error management features that IDispatch interfaces have. To generate the proper error information, an application must use the IErrorInfo object, which is provided by the operating system. A server need only support the ISupportErrorInfo interface, which lets an automation controller know that it should look at the IErrorInfo object for more information when an error occurs. The first step is to add an enumeration of the types of errors that the server can generate to the ODL file (see Listing 5.11). This step has the effect of publishing the error constants to the user of the automation server. The enumeration can be added anywhere within the ODL file and still produce the proper C++ declaration in the BCFServerInterfaces.h file. Remember to generate a new CLSID for the enumeration.
Listing 5.11 BCFSERVER.ODL--Error Enumeration in the ODL File coclass Tracker { [default] interface ITracker; }; typedef [uuid(AA3DFE23-5C1C-11d0-BEE7-00400538977D), helpstring("Tracker Error Constants")] enum tagTrackerError { MFCSERVER_E_NO_UBOUND = 46080, MFCSERVER_E_NO_LBOUND = 46081, MFCSERVER_E_NO_ARRAYLOCK = 46082, MFCSERVER_E_NO_FILE = 46083, MFCSERVER_E_BAD_ARRAY_PARAMETER = 46084, MFCSERVER_E_INVALID_VALUE = 46085 }TRACKERERROR;
};
The next step is to add the actual error generating code. Listing 5.12 shows the additional code that is added to the server implementation of OutputLines and put_Indent to enable error generation. BaseCtl provides a helper function Exception for generating rich error information. This function takes three parameters: an error number, a string resource ID, and a help context ID. See Chapters 3 and 4 for more information regarding rich error information.
Listing 5.12 TRACKER.CPP--Rich Error Information Added to Tracker Implementation STDMETHODIMP CTracker::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal) { HRESULT hResult = S_OK; *RetVal = VARIANT_TRUE; // if we have a file and if the variant contains a string array if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR)) { // lock the array so we can use it if(::SafeArrayLock(varOutputArray->parray) == S_OK) { LONG lLBound; // get the lower bound of the array if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK) { LONG lUBound; // get the number of elements in the array if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound) == S_OK) { . . . else { *RetVal = VARIANT_FALSE;
// create the error message hResult = this->Exception( MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_NO_UBOUND), IDS_E_NO_UBOUND, NULL); } } else { *RetVal = VARIANT_FALSE; // create the error message hResult = this->Exception( MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_LBOUND), IDS_E_NO_LBOUND, NULL); } // unlock the array we don't need it anymore ::SafeArrayUnlock(varOutputArray->parray); } else { *RetVal = VARIANT_FALSE; // create the error message hResult = this->Exception( MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_ARRAYLOCK), IDS_E_NO_ARRAYLOCK, NULL); } } else { *RetVal = VARIANT_FALSE; // if there wasn't a file if(!m_fileLog) // create the error message hResult = this->Exception( MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE), IDS_E_NO_FILE, NULL); else // create the error message hResult = this->Exception( MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_BAD_ARRAY_PARAMETER),
IDS_E_BAD_ARRAY_PARAMETER, NULL); } // return the result return hResult; } STDMETHODIMP CTracker::get_Indent(long FAR *Value) { HRESULT hResult = S_OK; // return the member variable *Value = m_lIndent; // return the result return hResult; } STDMETHODIMP CTracker::put_Indent(long Value) { HRESULT hResult = S_OK; // if the new value is at least 0 if(Value >= 0) // assign the value to our member variable m_lIndent = Value; else { // create the error message hResult = this->Exception( MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_INVALID_VALUE), IDS_E_INVALID_VALUE, NULL); } // return the result return hResult;
} Take care when defining your interfaces and how they communicate errors and invalid conditions. The use of rich error information is very helpful to the user of the component and can be as critical to the implementation as the methods and properties that the server supports.
Dual-Interface
In Chapter 3, the basic MFC server is implemented as IDispatch only, and you are required to add dual-interface support as an extra step. With BaseCtl, dual-interface support is built in and implemented as a normal aspect of the server.
Generating Dual-Interface OLE Exceptions Again as in Chapter 3, the basic MFC server is implemented as IDispatch only, and it is possible to throw standard C++ exceptions and have the basic MFC IDispatch support code translate the error into an OLE exception. When an MFC server is converted to dual-interface, you must implement the exception translation code yourself. For BaseCtl, the server has been implemented as dual-interface from the start, and all error generation has been written as true OLE exceptions and does not require translation as in MFC.
Server Instantiation Using C++ OLE is not the only method for creating and using automation servers. This chapter will show you how to create OLE servers using C++ syntax. At times, creating and using automation servers is necessary from within the application in which they are defined. Take, for example, a case where an application contains three servers, only one of which is directly creatable by outside applications using OLE. The remaining two servers can be created by the exposed server using C++ and returned via a method call to another application, which then uses the server as though it were created via OLE. For an MFC server, the inclusion or exclusion of the macros DECLARE_OLECREATE and IMPLEMENT_OLECREATE determined whether a server is creatable by external applications. For BaseCtl, it is a little simpler. All BaseCtl applications contain a global variable called ObjectInfo for declaring all of the servers that can be created via OLE. The ObjectInfo structure is declared in the main application file:
OBJECTINFO g_ObjectInfo[] = { EMPTYOBJECT
}; Each OLE server implemented within the application will have a single entry within the body of the ObjectInfo structure, thus identifying it as an exposed OLE server. The AUTOMATIONOBJECT macro defines the C++ class of the server that can be created:
OBJECTINFO g_ObjectInfo[] = { AUTOMATIONOBJECT(Tracker), EMPTYOBJECT
}; To prevent an application from being exposed as an automation server, you only need to remove or comment out the entry in the object map. On the opposite side, when adding additional creatable servers to an application, it is important to add an entry for each new server to the ObjectInfo structure. All BaseCtl servers contain a static function Create that is used to create instances of themselves. The Create function is the only way that you should instantiate a server since it is implemented by the class factory of the server and will manage the server creation correctly. This is critical in the cases where the server is shared among two or more applications, as you will see in the next sections. Listing 5.13 shows an example of creating a server using C++ syntax. Create returns an IUnknown *, which is used to retrieve the proper custom interface that can then be cast to the C++ class of the server. Remember to check all of the return values and pointers to ensure that all of the method calls returned successfully. Don't forget to increment and decrement the reference counts of the server to ensure that its lifetime is what you expect. You must also navigate to the correct interface using the function QueryInterface, as is the case in Listing 5.13, which is looking for the custom interface pointer of the IID_ITracker object. A simple cast from the Create function IUnknown * to the C++ class is not enough to get the correct function offsets.
Listing 5.13 Example--Creating an OLE Server Using C++ // create the server IUnknown * pUnk = CTracker::Create(NULL); // if we have an IUnknown if(pUnk) { // custom interface reference ITracker * pTrack = NULL; // QI for the custom interface and if successful if(pUnk->QueryInterface(IID_ITracker, (LPVOID *) &pTrack) == S_OK) { // too many ref counts - release one pTrack->Release(); // object reference CTracker * opTracker = NULL;
// cast to the object opTracker = (CTracker *) pTrack; // use the object opTracker->put_Indent(1); // release the object - this is the last one so the // object should be destroyed opTracker->Release(); } else // release the IUnknown - we couldn't find the custom interface pUnk->Release();
} After a server has been created this way, it can be used like any other C++ class or OLE server. Since the server is a running OLE Object, all of the standard OLE functions work, and the server can be used from other applications; for example, QueryInterface can be used to retrieve IDispatch or custom interface pointers that can be passed to other applications. See Chapter 3 for more information regarding the creation and use of OLE servers with C++. So far, you've looked only at how to create individual instances of objects. Next you will find out how to share objects.
Shared Servers OLE defines a facility, called the Running Object Table, for sharing objects. An object that is shareable will publish its CLSID and an IUnknown reference to itself in the Running Object Table. Any application that so desires can ask for the running instance of the object rather than create a new instance. This capability is useful for applications that may need to work with a single instance of an application rather than create multiple copies. The Tracker object is a perfect candidate for this kind of functionality. Multiple applications could use the same Tracker object to log information thus saving on memory. The first step is to register the server as running (see Listing 5.14). The BaseCtl Framework allows access to the basic ClassFactory operation of creating the server through the static Create function. Since this function is where all instances of the server are created, the Create function is the most logical location to register the server as running. After the new object is created, you should QueryInterface for an IUnknown reference to the server. If successful, you call the RegisterActiveObject passing the IUnknown and the CLSID of the server. This sample implementation passes the constant ACTIVEOBJECT_STRONG, which will result in the reference count of the server being incremented by one. See the VC++
documentation for more information regarding this parameter. You must also pass the address of a member variable to store the ID of the running object. The ID is used later when revoking the object from the Running Object Table. Last you must decrement the reference count of the server to offset the QueryInterface call.
Listing 5.14 TRACKER.CPP--Create Function Implementation of Shared Object Support IUnknown *CTracker::Create ( IUnknown *pUnkOuter ) { // make sure we return the private unknown so that we support aggegation // correctly! // CTracker *pNew = new CTracker(pUnkOuter); LPUNKNOWN pIUnknown = NULL; // QI for the IUnknown and if successful if(pNew->QueryInterface(IID_IUnknown, (LPVOID *) &pIUnknown) == S_OK) { // register the clsid as an active object // so other applications will get the same object // and if it didn't succeed - this function will // increment the reference count because of the ACTIVEOBJECT_STRONG if(::RegisterActiveObject(pIUnknown, CLSID_Tracker, ACTIVEOBJECT_STRONG, &pNew->m_dwRegister) != S_OK) // make sure that the reference ID is clear pNew->m_dwRegister = NULL; // remove the extra refcount from the QI pIUnknown->Release(); } return pNew->PrivateUnknown();
} You've added the code to register the server in the Running Object Table, but now you need to add code to remove the server from the table. The only practical way to remove the server from the Running Object Table is to monitor the reference counts of the server. When the reference count reaches one--indicating that the only reference left is the Running Object Table's reference--revoke the
server's reference from the Running Object Table. The only location that you can use to accurately monitor the reference count of the server is in the IUnknown::Release function implementation. To aid in the implementation of the IUnknown::Release function, we have supplied a new macro to handle the revoking the server from the Running Object Table (see Listing 5.15). The implementation checks to see whether an ID is present for the server and whether the reference count is one and revokes the server from the Running Object Table. The implementation also increments the reference count and clears the ID member variable to prevent the object from being destroyed before this function has completed and also to prevent the code from becoming recursive. Incrementing the reference count and clearing the member variable is necessary since the RevokeActiveObject function call will result in another call to Release to decrement the reference of the server. After RevokeActiveObject returns, the server removes its last reference count, which results in the destruction of the server.
Listing 5.15 RACKER.H-DECLARE_STANDARD_UNKNOWN_SHARED Macro Implementation #define DECLARE_STANDARD_UNKNOWN_SHARED() \ STDMETHOD(QueryInterface)(REFIID riid, void **ppvObjOut) { \ return ExternalQueryInterface(riid, ppvObjOut); \ } \ STDMETHOD_(ULONG, AddRef)(void) { \ return ExternalAddRef(); \ } \ STDMETHOD_(ULONG, Release)(void) \ { \ long lRefCount = this->ExternalRelease(); \ if(this->m_dwRegister && lRefCount == 1) \ { \ this->ExternalAddRef(); \ DWORD tdwRegister = this->m_dwRegister; \ this->m_dwRegister = 0; \ ::RevokeActiveObject(tdwRegister, NULL); \ return this->ExternalRelease(); \ } \ else \ return lRefCount; \ } \ DWORD m_dwRegister; The last step is to update the class declaration of your server to use the new macro (see Listing 5.16). Replace the DECLARE_STANDARD_UNKNOWN() with DECLARE_STANDARD_UNKNOWN_SHARED().
Listing 5.16 TRACKER.H--New Macro class CTracker : public ITracker, public CAutomationObject, ISupportErrorInfo { public: // IUnknown methods // DECLARE_STANDARD_UNKNOWN_SHARED(); // IDispatch methods // DECLARE_STANDARD_DISPATCH(); . . . During the lifetime of the server, an application is capable of getting the instance of the running server with a method call to OLE. In VB, you use the GetObject method call, and in VC++, you use the GetActiveObject function. After the pointer to the server is retrieved, the server can be used as though it were created through normal OLE mechanisms. This method of sharing objects is fine but requires that the developer of the application using the server take an active role in deciding to use the shared object versus creating a new instance of the object. Another approach can be taken, and that is to supply the instance of a running server to an application that calls CreateObject rather than GetObject. A server implemented to always return the same instance to any application is referred to as a single instance server.
Single Instance Servers A single instance server is used in the cases where you do not want the users of the component to have a choice in using a shared instance of the server or a copy that they create themselves. No matter what, you want to create only a single running instance of the server that is shared by all applications. To enable a BaseCtl server for single instance requires that you modify the code that was added to the Create function in the section "Shared Servers." Listing 5.17 shows the changes that you made to the sample implementation to enable single instance support. First, you must look to see whether the server is already registered as running. If not, a server is created and registered as running. If the server is already running, you must QueryInterface for the custom interface pointer so that you can cast to the correct C++ class type. The last step is to call Release since there will be an extra reference count from the GetActiveObject and QueryInterface calls.
Listing 5.17 TRACKER.CPP--Create Function Implementation Updated to Support Single Instance Servers
IUnknown *CTracker::Create ( IUnknown *pUnkOuter ) { CTracker *pNew = NULL; // Initialize an IUnknown reference LPUNKNOWN pIUnknown = NULL; // see if the object is already running ::GetActiveObject(CLSID_Tracker, NULL, &pIUnknown); // if we didn't get a reference to a running object if(!pIUnknown) { // make sure we return the private unknown so that we support // aggegation correctly! // pNew = new CTracker(pUnkOuter); // QI for the IUnknown and if successful if(pNew->QueryInterface(IID_IUnknown, (LPVOID *) &pIUnknown) == S_OK) { // register the clsid as an active object // so other applications will get the same object // and if it didn't succeed - this function will // increment the reference count because of the ACTIVEOBJECT_STRONG if(::RegisterActiveObject(pIUnknown, CLSID_Tracker, ACTIVEOBJECT_STRONG, &pNew->m_dwRegister) != S_OK) // make sure that the reference ID is clear pNew->m_dwRegister = NULL; // remove the extra refcount from the QI pIUnknown->Release(); } } else { // custom interface reference ITracker * pTrack = NULL; // QI for the custom interface and if successful if(pIUnknown->QueryInterface(IID_ITracker, (LPVOID *) &pTrack) == S_OK) { // too many ref counts - release one
pTrack->Release(); // cast to the object pNew = (CTracker *) pTrack; } else { // release the IUnknown - we couldn't find the custom interface pIUnknown->Release(); // go ahead and create an object pNew = new CTracker(pUnkOuter); // QI for the IUnknown and if successful if(pNew->QueryInterface(IID_IUnknown, (LPVOID *) &pIUnknown) == S_OK) { // register the clsid as an active // object so other applications will get the same object // and if it didn't succeed - this function will // increment the reference count because of the ACTIVEOBJECT_STRONG if(::RegisterActiveObject(pIUnknown, CLSID_Tracker, ACTIVEOBJECT_STRONG, &pNew->m_dwRegister) != S_OK) // make sure that the reference ID is clear pNew->m_dwRegister = NULL; // remove the extra refcount from the QI pIUnknown->Release(); } } } return pNew->PrivateUnknown(); } Shared server support is very straightforward to implement and use and adds a level of functionality not normally available to standard server implementations.
User Interface and Events Like MFC and ATL, BaseCtl servers can also benefit from the addition of User Interface (UI) and Events. Again, since BaseCtl does not offer integration with MFC, you will be forced to implement all of the UI with the standard Win32 functions, which is not a very appealing prospect. At the time this book was written, only Visual Basic 5 was capable of using event interfaces in automation servers.
From Here...
In this chapter, you created a simple implementation of a BaseCtl automation server. You also learned how to expand upon the basic framework provided by BaseCtl to create new and interesting features within your implementation. BaseCtl provides a simple and straightforward framework for creating Automation Servers. Unfortunately, the lack of MFC integration and support from Microsoft as a development platform will put it last on your list of choices when selecting a development direction. For those of you who are willing to or interested in learning more about Automation Servers and ActiveX architecture, the BaseCtl is a fine way to learn since it is so lean. The BaseCtl is probably better in this area than ATL because you won't have to understand the concept of templates while also learning ActiveX. The next six chapters take a look at implementing ActiveX controls using the same three tool sets: MFC, ATL, and BaseCtl.
Chapter 6 Using MFC to Create a Basic ActiveX Control ●
Using MFC to Create a Basic ActiveX Control ❍ Creating the Basic Control Project ❍ Control Registration ■ Listing 6.1 MFCCONTROLWINCTL.CPP--Default UpdateRegistry Implementation ❍ Creating Methods ■ Listing 6.2 MFCCONTROLWINCTL.H--Alignment Enumeration Include File and Member Variables Added to Class Definition ■ Listing 6.3 ALIGNMENTENUMS.H--Alignment Enumeration Include File ■ Listing 6.4 MFCCONTROLWINCTL.CPP--Initialize the m_lAlignment Member Variable in the Class Constructor ■ Listing 6.5 MFCCONTROLWINCTL.CPP--CaptionMethod Implementation ■ Listing 6.6 MFCCONTROL.ODL--Keyword [optional] Added to the CaptionMethod ODL Definition ❍ Properties ■ Creating Normal User Defined Properties ■ Listing 6.7 MFCCONTROLWINCTL.CPP--Alignment Property Get/Set Method Implementation ■ Creating Parameterized User Defined Properties ■ Listing 6.8 MFCCONTROLWINCTL.CPP--GetCaptionProp Implementation ■ Listing 6.9 MFCCONTROLWINCTL.CPP--SetCaptionProp Implementation ■ Listing 6.10 MFCCONTROL.ODL--[optional] Keyword Added to the ODL File ■ Creating Stock Properties ■ Using Ambient Properties ■ Creating Property Sheets ■ Listing 6.11 MFCCONTROLWINPPG.CPP--New Member Variables Added to the DoDataExchange Function ■ Listing 6.12 MFCCONTROLWINPPG.CPP--Updated DoDataExchange Function; Alignment Enumeration Added ■ Listing 6.13 MFCCONTROLWINCTL.CPP--BoundPropertyChanged Function within the SetAlignment Implementation ■ Listing 6.14 MFCCONTROLWINPPG.CPP--m_AlignmentValue Class Member Initialized in the Class Constructor ❍ Adding Events ■ Listing 6.15 MFCCONTROLWINCTL.H--FireChange Function Prototype Added to the Class Definition ■ Listing 6.16 MFCCONTROLWINCTL.CPP--FireChange Implementation Added to the MFControlWinCtl.cpp Source File ■ Listing 6.17 MFCCONTROWINCTL.CPP--FireChange Event Added to the CaptionMethod Implementation ❍ Persistence ■ Listing 6.18 MFCCONTROLWINCTL.CPP--Simple DoPropExchange Implementation ■ Listing 6.19 MFCCONTROLWINCTL.CPP--DoPropExchange Implementation Separated into Distinct Operations ■ Listing 6.20 EXAMPLE--Example Implementation of the Code Needed to Support Stock Property Persistence
❍
❍
Drawing the Control ■ Standard Drawing ■ Listing 6.21 MFCCONTROLWINCTL.H--CBrush Member Variable Added to CMFCControlWinCtrl ■ Listing 6.22 MFCCONTROLWINCTL.CPP--Standard Drawing Added to the OnDraw Function From Here...
Using MFC to Create a Basic ActiveX Control ●
●
●
●
●
Registration Registration is probably the most important aspect of creating a control. MFC registration is robust and easy to understand. Adding methods and properties The method is the basis for all communication to the control. Like methods, properties are a way to expose information about a control implementation to the control's user. The MFC ClassWizard makes adding methods and properties easy. Adding events The control uses events to communicate information and conditions to the control's user. Persistence Persistence is the control's means to remember information about itself across execution lifetimes. MFC persistence is based on a set of macros. Drawing the control Drawing the User Interface can make or break a control implementation.
Visual C++ and MFC are powerful and flexible tools for creating ActiveX controls. In terms of rapid development and ease of use, these two win hands down. In this chapter, you will see just how powerful these tools are. You will create an ActiveX control with all the basics: methods, properties, events, persistence, and drawing. And you will explore some of the more advanced features and lesser known aspects of control development, such as methods with optional parameters, asynchronous properties, Clipboard support, and optimized drawing, to name just a few.
NOTE: MFC controls are linked dynamically to the MFC DLLs for support and implementation. Linking dynamically can impair the control's load time because the MFC DLLs are loaded in addition to the control itself. Even though the MFC DLLs may already be in memory, you still have DLL address fixups and memory swapping issues. When developing an ActiveX control, the developer is prudent to remove as much dependence on MFC as possible. This practice serves two purposes: First, this practice makes it much easier to port the control to an alternative framework such as ATL or BaseCtl; second, it improves the overall performance of the control because you don't have the overhead of the MFC DLLs to contend with. As a general rule, components that are performance-sensitive should avoid using DLLs of any kind. The last thing you want is a component that is disk-bound.
Creating the Basic Control Project
To create an MFC ActiveX control, you want to take advantage of the AppWizard provided by Visual C++. Run the Visual C++ development environment, and from the File menu select, New. When the New dialog displays (see fig. 6.1), select the Projects tab. The Projects tab allows you the opportunity to define several aspects of how the application will be created, for example, the type of application to create, the name of the application, and the location where you want the project created. For the type, select MFC ActiveX ControlWizard; enter the Project Name MFCControl, and the Location will be C:\que\ActiveX\MFCControl. Click the OK button to start the ControlWizard so you can define the properties of your control. FIG. 6.1 Define the new MFC control project with the New dialog. The first step in the ControlWizard allows you to define how many controls will exist within your project (see fig. 6.2). Using the up and down arrow buttons, select 3 for the number of controls, and leave the remainder of the properties set to their default values. Click the Next button to continue. FIG. 6.2 MFC ActiveX ControlWizard -- Step 1 of 2 is where you define the number of controls in your project.
TIP: If you have a requirement to create more than one control, and the controls are often used together within the same application, we recommend combining the controls into a single .ocx file--because of .ocx load times. A single .ocx always loads faster than two .ocxs of comparable size. Code sharing can also be an advantage, and the end result is a control that's smaller and loads faster than the two individual controls combined.
Step 2 of the ControlWizard allows you to change the name of the control, some of its features, and whether the control will be a subclass of an existing window's control (see fig. 6.3). FIG. 6.3 Use the ControlWizard -- Step 2 of 2 to further define your control. Because you have instructed the Control Wizard that you want 3 controls for this project, you will rename MFCControl1, MFCControl2, and MFCControl3 to MFCControlWin, MFCControlNoWin, and MFCControlSubWin, respectively. To rename a class, select the name in the drop-down list box and click the Edit Names button. Enter the new name of the control in the Short Name edit box (see fig. 6.4). The names of the other files and classes will change automatically to conform to the new Short Name. Click OK to close the Edit Names dialog. FIG. 6.4 Edit the names of the control using the Edit Names dialog. The Advanced button is used to display a dialog for selecting the ActiveX enhancements that you want for the control (see fig. 6.5). Ensure that the MFCControlWin class is selected, and click the Advanced button. FIG. 6.5 Select from the Advanced ActiveX Features dialog for your control. All of the advanced features in the dialog are the result of the OC 96 specification referred to in Chapter 1. Now take a minute to look at the features in detail, as shown in Table 6.1. Table 6.1 Advanced ActiveX Features
ActiveX Feature
Description
Windowless activation
Allows the control to be instantiated without having to create a window for itself--relies on the container to do so.
Unclipped device context
Used for controls that guarantee they will not draw outside their client area and results in disabling MFC clipping tests for the control. Cannot be used with a windowless control.
Flicker-free activation
Used for controls that appear the same to the user whether the control is in an active or inactive state. Selecting this feature eliminates some of the flicker associated with changing states between the active and inactive states of a control. Cannot be used with a windowless control.
Mouse pointer notifications when inactive Enables control to receive mouse messages, even though the control is in an inactive state. Optimized drawing code
Allows the control to draw its User Interface in an optimized fashion if the container supports optimized drawing; otherwise, the control must be drawn using standard drawing techniques.
Loads properties asynchronously
Asynchronous properties are properties that load in thebackground and involve the transfer of a large amount of data. The properties are loaded in the background to prevent the container and the control from being disabled or inactive for too long while waiting for the data to load.
From the Advanced ActiveX Features dialog, select only the following options (refer to fig. 6.5): ● ● ● ● ●
Unclipped device context Flicker-free activation Mouse pointer notifications when inactive Optimized drawing code Loads properties asynchronously
Click OK to confirm the selection and close the dialog. Select the MFCControlNoWin control, and click the Advanced button again. You are going to implement MFCControlNoWin as a windowless control just to get a feel for the difference between the windowed and windowless control implementations in MFC. From the Advanced ActiveX Features dialog, select only the following options (refer to fig. 6.5): ● ● ● ●
Windowless activation Mouse pointer notifications when inactive Optimized drawing code Loads properties asynchronously
Click OK to confirm the selections and close the dialog. For your third and final control, you subclass an existing window's control. Be sure to select the control MFCControlSubWin, and in the subclassing list box, select BUTTON. Do not define any advanced ActiveX features for this control. The MFC ActiveX ControlWizard -- Step 2 of 2 dialog also provides other features for further defining your control project. For the sample project, leave the values in their default state. See the VC++ documentation for more information regarding other available options.
Click the Finish button on the ControlWizard dialog to complete the feature selection process (refer to fig. 6.3). The New Project Information dialog displays to let you confirm the choices you made (see fig. 6.6). Click OK to generate the source files and the MFC control project. FIG. 6.6 Make your confirmations in the New Project Information dialog. The first step in any control project is to ensure that it contains registration support. Without registration, the control cannot be used by any application.
Control Registration Control registration and unregistration support is provided for you by MFC. You are not required to make any code changes or additions to support it. Listing 6.1 shows the UpdateRegistry function that was created for the control by the AppWizard; each control will have its own registration function. One thing of interest is the comment from Microsoft regarding apartment model threading and how you should register your control if you know it doesn't conform to apartment model rules. Other than that, you will not have to make any changes to your application or this function when it comes to control registration.
Listing 6.1 MFCCONTROLWINCTL.CPP--Default UpdateRegistry Implementation ///////////////////////////////////////////////////////////////////////////// // CMFCControlWinCtrl::CMFCControlWinCtrlFactory::UpdateRegistry // Adds or removes system registry entries for CMFCControlWinCtrl BOOLCMFCControlWinCtrl::CMFCControlWinCtrlFactory::UpdateRegistry(BOOLbRegister) { // TODO: Verify that your control follows apartment-model threading rules. // Refer to MFC TechNote 64 for more information. // If your control does not conform to the apartment-model rules, then // you must modify the code below, changing the 6th parameter from // afxRegApartmentThreading to 0. if (bRegister) return AfxOleRegisterControlClass( AfxGetInstanceHandle(), m_clsid, m_lpszProgID, IDS_MFCCONTROLWIN, IDB_MFCCONTROLWIN, afxRegApartmentThreading, _dwMFCControlWinOleMisc, _tlid, _wVerMajor, _wVerMinor); else return AfxOleUnregisterClass(m_clsid, m_lpszProgID); } You can now compile and register the control you've created, but it won't be of much use because it doesn't contain methods, properties, or events, which are the basis of every ActiveX Control.
Creating Methods Now that you've successfully created your ActiveX control project, you can start off by adding a method, which is one of the basic aspects of component development. For the purposes of the sample control, you are going to add a method called CaptionMethod. The method will accept two parameters, the second one being optional. The first parameter is a string that the control will display within its client area, and the second, optional parameter is the alignment of the caption within the client area, either left, right, or center. From the View menu, select ClassWizard, and in the MFC ClassWizard dialog (see fig. 6.7), select the Automation tab. FIG. 6.7 Adding a method is done through the MFC ClassWizard. From the Class Name drop-down list box, select the CMFCControlWinCtrl class, and click the Add Method button to create a new method. Type CaptionMethod in the External Name combo box (see fig. 6.8), and set the Return type to a long. Add two parameters--by clicking the appropriate column in the Parameter List control--the first called lpctstrCaption of type LPCTSTR, and the second, as your optional parameter, called varAlignment of type VARIANT. Click OK to add the method to the class. Next click OK to close the ClassWizard dialog. FIG. 6.8 Add the CaptionMethod method to the control class.
NOTE: All optional parameters must be of type VARIANT, and they must fall at the end of the parameter list. Optional parameters are not managed in any way by OLE. It is the server application's responsibility to determine whether the VARIANT parameter passed to the method contains data and ●
to either use the data passed to the method or convert the data to a useful type, if possible, or
●
to ignore the parameter if invalid data was passed and use the default value if appropriate, or
●
to inform the user of an error condition if one of the above conditions was not met.
To aid your CaptionMethod implementation, you need to add an enumeration for all the valid alignment settings and two member variables to your class definition (see Listing 6.2). The enumeration is included in the header file Alignmentenums.h (see Listing 6.3). The two member variables, m_cstrCaption and m_lAlignment, are used to store the caption string and the alignment setting while the control is being used. Note the type used for the m_lAlignment member variable. The variable is declared as type long and not as the enumeration type because of the data type restrictions that are imposed upon you by ActiveX Automation. Remember that only data types that can be passed in a VARIANT can be used in methods and properties. By declaring the m_lAlignment member as long, you do not have to explicitly convert the value by casting to the enumerated type when it is retrieved from the VARIANT parameter in the caption method. On the other hand, casting the value to the enumerated type is a trivial issue and is completely up to you to implement if you desire to do so.
Listing 6.2 MFCCONTROLWINCTL.H--Alignment Enumeration Include File and Member Variables Added to Class Definition . . .
// MFCControlWinCtl.h : Declaration oftheCMFCControlWinCtrlActiveXControl #include "alignmentenums.h" ///////////////////////////////////////////////////////////////////////////// // CMFCControlWinCtrl : See MFCControlWinCtl.cpp for implementation. class CMFCControlWinCtrl : public COleControl { . . . protected: // storage variable for the caption CString m_cstrCaption; // storage variable for the alignment long m_lAlignment; }; . . . The enumeration is added as an include file (see Listing 6.3) so that it can be referenced by other files, which will be necessary as you proceed through the chapter.
Listing 6.3 ALIGNMENTENUMS.H--Alignment Enumeration Include File #if !defined _ALIGNMENTENUMS_H #define _ALIGNMENTENUMS_H // caption alignment enumeration typedef enum tagAlignmentEnum { EALIGN_LEFT = 0, EALIGN_CENTER = 1, EALIGN_RIGHT = 2, }EALIGNMENT; #endif // #if !defined _ALIGNMENTENUMS_H Open the MFCCOntrolWinCtl.cpp file, and in the constructor initialize the m_Alignment member variable to the value EALIGN_LEFT (see Listing 6.4).
Listing 6.4 MFCCONTROLWINCTL.CPP--Initialize the m_lAlignment Member Variable in the Class Constructor ///////////////////////////////////////////////////////////////////////////// // CMFCControlWinCtrl::CMFCControlWinCtrl - Constructor CMFCControlWinCtrl::CMFCControlWinCtrl() { InitializeIIDs(&IID_DMFCControlWin, &IID_DMFCControlWinEvents); m_lReadyState = READYSTATE_LOADING; // TODO: Call InternalSetReadyState when the readystate changes. // set the alignment to the default of left m_lAlignment = EALIGN_LEFT; } The CaptionMethod contains all of the code for setting the caption and the alignment style (see Listing 6.5) and does several things of interest that need to be pointed out.
First the method tries to deal with the optional parameter by looking for the data type you really want, VT_I4, which is a data type of long. If the VARIANT parameter doesn't contain a long, the method then checks to see if any data was passed at all, by checking for VT_ERROR or VT_EMPTY. This is necessary because the parameter is optional. A variant parameter can either contain data or not. It is important to check VARIANT data types not only for valid data but also for the actual existence of data. If no data was supplied, the method relies on the value already contained in the m_lAligmnent class member variable.
Automation Controllers and Optional Parameters Some automation controllers, such as Visual Basic, do not require that any or all of the optional parameters be supplied when calling a method, for example, the CaptionMethod can be called as MyObject.CaptionMethod "Hello" or MyObject.CaptionMethod "Hello", True from Visual Basic. Visual Basic automatically passes in aVARIANT set to the type VT_ERROR for any parameters that are omitted from a method call. For automation controllers that call the standard OLE Invoke method, such as C++, all the parameters must be defined, even though they may not be used. In this case, when calling the method, the programmer has the option of setting the VARIANT type to VT_ERROR or VT_EMPTY. When processing optional parameters, relying on the OLE VARIANT conversion routines in addition to looking for specific VARIANT data types guarantees that your control can handle any data passed to it.
If the VARIANT is of a valid data type other than VT_I4, the method tries to convert it to a VT_I4 type. This is for cases where a user passes valid data in the form of a different data type, for example, a short or a string. One thing to note is the use of the function VariantInit. It is very important that all VARIANT variables be initialized prior to their use. This practice will guarantee that the VARIANT does not contain invalid data type information or invalid values. This follows the basic C++ tenet of initializing all member variables to ensure that they do not contain invalid information. If the requirements of your control demand that you deal with only specific data types, you can also add code (error messages, exceptions, and so on) to deal with the fact that the method did not receive a valid data type. If the function VariantChangeType was unable to convert the data, the method exits and returns a value of FALSE. A return of FALSE indicates to the caller of the method that the method didn't succeed. Again, you can add additional error handling code to the method to give the user more information about the error that occurred. See Chapters 3 through 5 on generating OLE exceptions for more information. Before proceeding, the method ensures that the m_lAlignment member variable contains valid data. If the method received valid data, or converted the data to a valid value, as indicated by the variable lResult equaling TRUE, it stores the caption and the alignment values in the class member variables, invalidates the control so it will redraw its User Interface (UI) based on the new information, and exits the function.
Listing 6.5 MFCCONTROLWINCTL.CPP--CaptionMethod Implementation
long CMFCControlWinCtrl::CaptionMethod(LPCTSTR lpctstrCaption, const VARIANT FAR& varAlignment) { // return value initialized to failure result long lResult = FALSE; // if the variant is a long just use the value if(VT_I4 == varAlignment.vt) { // assign the value to our member variable m_lAlignment = varAlignment.lVal; // set the return value lResult = TRUE; } // if the user didn't supply an alignment parameter we will use whatever is already there else if(VT_ERROR == varAlignment.vt || VT_EMPTY == varAlignment.vt) { // set the return value lResult = TRUE; } else { // get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType(&varConvertedValue, (VARIANT *) &varAlignment, 0, VT_I4)) { // assign the value to our member variable switch(varConvertedValue.lVal) { case EALIGN_CENTER: m_lAlignment = EALIGN_CENTER; break; case EALIGN_RIGHT: m_lAlignment = EALIGN_RIGHT; break; default: m_lAlignment = EALIGN_LEFT; break; } // set the return value lResult = TRUE; } else { // at this point we could either throw an error indicating there was a problem converting // the data or change the return type of the method and return the HRESULT value from the
// the "VariantChangeType" call. } } // if the alignment value is invalid if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT) // set to the default value m_lAlignment = EALIGN_LEFT; // if everything was OK if(TRUE == lResult) { // if we have a string if(lpctstrCaption != NULL) // assign the string to our member variable m_cstrCaption = lpctstrCaption; // did they pass us bad data? if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT) // sure did, lets fix their little red wagon m_lAlignment = EALIGN_LEFT; // force the control to repaint itself this->InvalidateControl(); } // return the result of the function call return lResult; } To complete your implementation of the CaptionMethod, you need to modify the ODL file. When methods, properties, and events are added to a class with the ControlWizard, the ODL file is also updated with the new entries. The ODL file is compiled into a type library, which is essentially a description of the component and its interfaces that can be used by other applications to access the component. You need to add the keyword [optional] to the last parameter in the CaptionMethod as in Listing 6.6. Doing so lets the container application know that the last parameter is optional and should be dealt with accordingly.
NOTE: Take care when modifying the ODL file by hand. The ODL file must match your implementation exactly. If the file is changed incorrectly, it can introduce strange bugs and/or implementation problems with your component. The ClassWizard automatically updates the ODL file when methods, properties, and events are added or removed. Adding or removing information from the ODL could prevent the ClassWizard from correctly managing the file. Keywords such as [optional] do not affect the ClassWizard and its capability to automatically update the file when changes are made to a class.
Listing 6.6 MFCCONTROL.ODL--Keyword [optional] Added to the CaptionMethod ODL Definition methods: // NOTE - ClassWizard will maintain method information here. // Use extreme caution when editing this section.
//{{AFX_ODL_METHOD(CMFCControlWinCtrl) [id(1)] long CaptionMethod(BSTR lpctstrCaption, [optional] VARIANT varAlignment); //}}AFX_ODL_METHOD
Properties Properties can be categorized as user defined, stock, or ambient. User defined properties are properties that are implementation-specific and have meaning only to the component that contains them. User defined properties can be further broken into those properties that are defined only as their specific data type (normal properties) and those with additional parameters (parameterized properties). Stock properties are a set of properties that are already defined by OLE in terms of the basic meaning. Stock properties are not implemented in the control of the container by default. They still require implementation by the control developer. They are predefined only to imply a certain level of uniformity between various control implementations. Ambient properties, on the other hand, are properties that are supported by the container to provide a default value to the control that uses them. In the remainder of the chapter, you will create three types of properties: normal, parameterized, and stock. You will also learn how to use ambient properties.
Creating Normal User Defined Properties A normal property is a property that is declared as a single type, for example, long or BSTR, and has no parameters. You will expose your controls' Alignment member variable through a property. Properties are added in much the same way as methods. Open the ClassWizard, select the class CMFCControlWinCtrl, switch to the Automation tab, and click the Add Property button. In the Add Property dialog (see fig. 6.9), set the External name to Alignment, the Type to long, and the Implementation to Get/Set methods. Click OK to confirm the entry and close the dialog. FIG. 6.9 Add the Alignment property to the Class Definition. Double-click the Alignment property entry in the External name list box to open the source file so you can add your code to the Get/Set methods. As you can see, Listing 6.7 takes advantage of the member variable m_Alignment, which you added earlier, and uses it to get and set the property value. The GetAlignment function is simple in that it returns only the value stored in the m_lAlignment member variable. The SetAlignment function does a little more. This function checks to see if the value is within the valid ranges of values and, if so, stores the value in the m_lAlignment member variable. The function then calls the SetModifiedFlag and the BoundPropertyChanged functions to notify the control and the container, respectively, that the value of the property has changed. BoundPropertyChanged has the effect of forcing the container to refresh its property browser to reflect the new value. This is very important because the value of the
property could change without the container's knowledge, either through the control's property sheet or, in some cases, in response to another function call. You might be asking yourself, "Why didn't I add BoundPropertyChanged to the CaptionMethod?" Well, you could have, but it wouldn't do much because the CaptionMethod can never be executed while the control is in design mode, which is the purpose of BoundPropertyChanged. The last thing the SetAlignment method does is invalidate the control's UI so it will repaint using the new information.
NOTE: Even though a property appears as a single member of an object, it is really a pair of related functions used to operate on a single piece of data. The ODL entries for a property will differ depending on its implementation. For interfaces inherited from a dispinterface class, it is enough to have a single entry in the properties section of the class. IDispatch inherited interfaces will define two separate functions: one declared as propertyget, and the other defined as propertyput, both sharing the same dispid. Parameterized properties are always defined by using the latter method because it is impossible to define the additional parameters without a method declaration. The dispinterface keyword is a specific convention for defining IDispatch-based interfaces in a more C++-like fashion, as opposed to the standard IDispatch style, which would be entered as one variable entry versus two method entries. For more information regarding the use of dispinterface and other ODL keywords, see the ODL documentation provided with the VC++ compiler.
Listing 6.7 MFCCONTROLWINCTL.CPP--Alignment Property Get/Set Method Implementation long CMFCControlWinCtrl::GetAlignment() { // return our current setting return m_lAlignment; } void CMFCControlWinCtrl::SetAlignment(long nNewValue) { // if we are in the valid range for the property if(nNewValue >= EALIGN_LEFT && nNewValue SetModifiedFlag(); // refresh the property browser this->BoundPropertyChanged(dispidAlignment); // redraw the control this->InvalidateControl(); } } MFC and the ClassWizard provide another option when declaring properties-- that is, by member variable. MFC will add a member variable of the appropriate type to the class declaration and will provide a notification function if the data changes. The notification message is defined as Onvariable_nameChanged and is added to the source file of
the control. If you had chosen this method for the Alignment member, the function would have been called OnAlignmentChanged and would have been implemented in the MFCControlWinCtl.cpp file. The variable style results in less code to write, but it also results in less flexibility because MFC manages all of the Get/Set property code for you. Feel free to experiment with both methods of property creation to get a feel for your best option.
Creating Parameterized User Defined Properties A parameterized property is a property that, in addition to being of a specific type (for example, BSTR or long), accepts one or more additional parameters to further define the data of the property. Parameterized properties can be useful for properties that represent collections of data where the additional parameter is the index into the collection. You are going to expose the controls m_cstrCaption member variable as a parameterized property in addition to your CaptionMethod function. In the Add Property dialog (see fig. 6.10), define the External name as CaptionProp, its Type as BSTR, and its Implementation as Get/Set methods. In addition, add a parameter called varAlignment of type VARIANT to the Parameter list box. Click OK to add the property to the class. Close the ClassWizard so you can add some code to your property. FIG. 6.10 Add the Caption property with the Add Property dialog. The implementation of the property CaptionProp has two methods: GetCaptionProp and SetCaptionProp. GetCaptionProp is the method that is called to return data from the property. In your implementation, you ignore the alignment parameter because it is of no use to you in this context; you simply return the caption (see Listing 6.8). GetCaptionProp uses the CString function AllocSysString to create a BSTR, which is essentially a UNICODE string, that is returned from the function call.
Listing 6.8 MFCCONTROLWINCTL.CPP--GetCaptionProp Implementation BSTR CMFCControlWinCtrl::GetCaptionProp(const VARIANT FAR& varAlignment) { // return the caption as a BSTR return m_cstrCaption.AllocSysString(); } SetCaptionProp simply defers to the CaptionMethod implementation because the CaptionMethod already does everything that you need (see Listing 6.9).
Listing 6.9 MFCCONTROLWINCTL.CPP--SetCaptionProp Implementation void CMFCControlWinCtrl::SetCaptionProp(const VARIANT FAR& varAlignment, LPCTSTR lpszNewValue) { // use the "CaptionMethod" implementation if(TRUE == this->CaptionMethod(lpszNewValue, varAlignment)) // let the container know that the property has changed this->SetModifiedFlag(); } Your final detail is the ODL file (see Listing 6.10). You are going to make the VARIANT, varAlignment, an
optional parameter for the GetCaptionProp and SetCaptionProp implementations. You can make the varAlignment parameter optional for the SetCaptionProp implementation because the varAlignment variable is the last in the list of actual parameters. The BSTR lpszNewValue variable is actually the data type of the property function pairs and does not affect the parameter definition rules.
Listing 6.10 MFCCONTROL.ODL--[optional] Keyword Added to the ODL File methods: // NOTE - ClassWizard will maintain method information here. // Use extreme caution when editing this section. //{{AFX_ODL_METHOD(CMFCControlWinCtrl) [id(2)] long CaptionMethod(BSTR lpctstrCaption, [optional] VARIANT varAlignment); [id(3), propget] BSTR CaptionProp([optional] VARIANT varAlignment); [id(3), propput] void CaptionProp([optional] VARIANT varAlignment, BSTR lpszNewValue); //}}AFX_ODL_METHOD
NOTE: Because of the parameters they contain, parameterized properties are defined in the methods section of the dispinterface declaration of an object. All properties are implemented using the dual method style in IDispatch-based interfaces regardless of whether the property is parameterized or not. Only dispinterfaces contain a separate property and method section in the ODL. And only dispinterfaces allow for properties to be declared as a single line entry in the ODL file.
Creating Stock Properties A stock property is a property that is understood by both a control and its container and that has a predefined meaning to both. Stock properties are intended to provide basic uniform functionality to all the controls and containers that implement them. Stock properties do not require you to implement a lot of code; you just hook into the existing property. Adding stock properties is similar to adding user defined properties. Open the ClassWizard, select the Automation tab, select the class CMFCControlWinCtrl, and click the Add Property button. Instead of typing an External name this time, you select one from the list provided. Your stock property will be the BackColor property (see fig. 6.11). Select the Stock Implementation, and click OK to add the property to the class. FIG. 6.11 Add BackColor stock property to the class definition. That's all there is to it. The BackColor property now appears in your property list and will persist with all your other properties. Whenever you need the value of a stock property, you can query the container for the property with one of the stock property functions. In your case, you will use the function GetBackColor. All of the stock properties have an associated Get/Set function pair. See the MFC documentation for more information. In addition to the default stock implementation style, you can support a member variable implementation. This option eliminates the need to query the container for the property but forces you to carry the value with you at all times, adding to your instance size. You can also support the Get/Set implementation method and store or use the value in anyway you see fit, but this
option also requires additional code. The default stock implementation is obviously the easiest because MFC does all the work for you.
Using Ambient Properties Ambient properties are properties implemented in the container in which the control resides, as opposed to stock properties, which are implemented in the control and not the container. Ambient properties share the same set of predefined meanings and dispids as those of stock properties. A dispid is the unique identifier that is used to identify the property within an interface. To use an ambient property, the control need request only the property value from the container and apply it in whatever manner is appropriate for the property type. The use of ambient properties allows the control to conform to the same settings as those of the container in which it resides. This provides much better integration between the control and its container. Take the previous example of adding the BackColor stock property to the sample control implementation. Defined as a stock property, the user of the control can change the background color of the control or leave it as is. If the color is different from that of the container or if the container's background color changes for some reason, the colors won't match, giving the appearance of a poorly integrated and written application. However, if the control simply used the ambient background color of its container, the control's background will always match that of the container. The specific requirements of your control implementation will decide which route you choose when implementing the properties your control supports. To access an ambient property, you can call one of the many ambient property functions defined in the COleControl class, for example, AmbientBackColor(), or use the function this->GetAmbientProperty(DISPID_BACKCOLOR, VT_COLOR, &varBackColor); to access the value. The GetAmbientProperty() function takes a dispid as its first parameter. The dispid must be one of those defined by MFC. A complete list of dispids can be found in the MFC source files. The second parameter is the VARIANT data type that is being requested, and the third parameter is a VARIANT variable into which the data is stored.
Creating Property Sheets Property sheets are a way for a control to display its properties for review and editing and are typically implemented in a tabbed dialog format. The original intent of property sheets was for use in cases when a control container did not support property browsing facilities. While property sheets have their purpose, they are probably not necessary for all implementations. Removing the property sheets from your control implementation definitely reduces the size of your control and does not take away from its implementation. Because property sheets are tabbed dialogs, most of your work will be done with the dialog editor and the ClassWizard. Select the Resource View in the Project Workspace window. From the list of dialogs, select IDD_PROPPAGE_MFCCONTROLWIN, and double-click the entry to open the resource editor. Using the resource editor, remove the static text control with the caption TODO, and place a static text control and a combo box control on the dialog (see fig. 6.12). FIG. 6.12 Use the resource editor to add controls to the Property Sheet dialog.
Using the mouse, select the label control on the form, and click the right mouse button. In the menu that appears, select the Properties menu item. On the General tab, set the ID of the control to IDC_ALIGNMENTLABEL and set the Caption to Alignment. Select the Styles tab and set the Align Text property to Right. Close the dialog to save the information. Again, using the mouse, select the combo box, use the right mouse to click on the control, and in the menu that appears, select the Properties menu item. On the General tab, set the ID of the control to IDC_ALIGNMENTCOMBO. On the Styles tab, set the T_ype to drop-down list box, and uncheck the Sort check box. Close the dialog to save the information. You have placed your two controls onto the property sheets and successfully modified their properties. Now you need to add some code to complete the implementation. Close the resource editor, and open the ClassWizard. Select the CMFCControlWinPropPage class, and select the Member Variables tab. In the Control ID's combo box, select IDC_ALIGNMENTCOMBO, and click the Add Variable button. In the Add Member Variable dialog (see fig. 6.13), set the Member variable name to m_AlignmentCombo and the Category to Control. Click OK to confirm the information, and close the dialog. FIG. 6.13 Add the m_AlignmentCombo member variable to the class definition. You've added a member variable for the control. Now you add one for the data. Click the Add Variable button again, and set the Member variable name to m_AlignmentValue (see fig. 6.14). Set the Category to Value and the Variable type to int. Click OK to confirm the changes, and close the dialog. FIG. 6.14 Add the m_AlignmentValue member variable to the class definition. Close the MFC ClassWizard dialog to confirm the new member variables. DoDataExchange is where you are going to add the code to manage the data between the control and the property sheet. Open the source file MFCControlWinPpg.cpp, and locate the DoDataExchange function. Notice that the ClassWizard added two lines of code to the original implementation of DoDataExchange (see Listing 6.11).
Listing 6.11 MFCCONTROLWINPPG.CPP--New Member Variables Added to the DoDataExchange Function void CMFCControlWinPropPage::DoDataExchange(CDataExchange* pDX) { //{{AFX_DATA_MAP(CMFCControlWinPropPage) DDX_Control(pDX, IDC_ALIGNMENTCOMBO, m_AlignmentCombo); DDX_CBIndex(pDX, IDC_ALIGNMENTCOMBO, m_AlignmentValue); //}}AFX_DATA_MAP DDP_PostProcessing(pDX); } DDX_Control is the standard MFC macro for loading a control into an MFC class member. DDX_CBIndex is a standard MFC function for getting and setting the current index in a combo box using the variable supplied, in this case m_AlignmentValue. You need to change this implementation slightly to fully bind your control and property sheet (see Listing 6.12).
First you load the combo box with valid property data so that the property sheet matches the property values in the control and the property browser. The function loads the combo box with only the valid choices if the DoDataExchange function is being executed the first time by checking the m_bSaveAndValidate member variable; in other words, the property dialog is not saving and validating the data it contains. To support communication between the control and the property sheet, you need to add DDP_CBIndex before the DDX_CBIndex line. DDP_CBIndex instructs MFC to obtain the Alignment property value from the control and store it in the m_lAlignment member variable of the property dialog when the dialog is loading. When the property dialog is unloading, DDP_CBIndex retrieves the current index of the combo box and sets the Alignment property of the control to the new value. DDP_CBIndex must be before the DDX_CBIndex function; otherwise, the property sheet will not correctly reflect the values of the properties in the control.
NOTE: Because of the specific implementation requirements of this property dialog (that is, the list box control is being loaded into the member variable, m_AlignmentCombo, of the class, and it is necessary to load the combo box selection list prior to setting the current selected entry in the list), the DDP_CBIndex and DDX_CBIndex lines had to be removed from between the MFC AFX_DATA_MAP macros to allow the setting of the entries to occur. Separating the lines in this fashion is not required in order to implement the DDX_CBIndex and DDP_CBIndex lines. It is, however, the only way to solve this particular problem. When the DDX_CBIndex and DDPCBIndex lines are removed from between the MFC macros, they no longer appear as a member variable in the ClassWizard and are not managed automatically by VC++ and the ClassWizard.
Listing 6.12 MFCCONTROLWINPPG.CPP--Updated DoDataExchange Function; Alignment Enumeration Added void CMFCControlWinPropPage::DoDataExchange(CDataExchange* pDX) { //{{AFX_DATA_MAP(CMFCControlWinPropPage) DDX_Control(pDX, IDC_ALIGNMENTCOMBO, m_AlignmentCombo); //}}AFX_DATA_MAP if(!pDX->m_bSaveAndValidate) { // make sure that we have cleared the list m_AlignmentCombo.ResetContent(); m_AlignmentCombo.AddString(_T("Left")); m_AlignmentCombo.AddString(_T("Center")); m_AlignmentCombo.AddString(_T("Right")); } DDP_CBIndex(pDX, IDC_ALIGNMENTCOMBO, m_AlignmentValue, _T("Alignment")); DDX_CBIndex(pDX, IDC_ALIGNMENTCOMBO, m_AlignmentValue); DDP_PostProcessing(pDX); this->SetModifiedFlag(); } As was pointed out earlier in this chapter, in order for the property browser within the IDE (for example, Microsoft Visual Basic's property list window) to accurately reflect the property value after it has been changed by the Property Sheet, you must add a BoundPropertyChanged call to your SetAlignment function (see Listing 6.13). This action notifies the property browser that the value has changed and that it should be retrieved again.
Listing 6.13 MFCCONTROLWINCTL.CPP--BoundPropertyChanged Function within the SetAlignment Implementation void CMFCControlWinCtrl::SetAlignment(long nNewValue) { // if we are in the valid range for the property if(nNewValue >= EALIGN_LEFT && nNewValue SetModifiedFlag(); // refresh the property browser this->BoundPropertyChanged(dispidAlignment); // redraw the control this->InvalidateControl(); } } The only thing remaining is to set the default value of the member variable m_AlignmentValue to a meaningful value (see Listing 6.14). Add the include file entry #include :MFCControlWinCtl.h" to the source file to resolve the EALIGN_LEFT constant. Initialize the m_AlignmentValue member variable in the constructor to a value of EALIGN_LEFT.
Listing 6.14 MFCCONTROLWINPPG.CPP--m_AlignmentValue Class Member Initialized in the Class Constructor // MFCControlWinPpg.cpp : Implementation of the CMFCControlWinPropPage property page class. #include "stdafx.h" #include "MFCControl.h" #include "MFCControlWinCtl.h" #include "MFCControlWinPpg.h" . . . CMFCControlWinPropPage::CMFCControlWinPropPage() : COlePropertyPage(IDD, IDS_MFCCONTROLWIN_PPG_CAPTION) { //{{AFX_DATA_INIT(CMFCControlWinPropPage) m_AlignmentValue = EALIGN_LEFT; //}}AFX_DATA_INIT }
Adding Events Properties and methods are a way for a programmer to communicate with a control from within the container application. Events are a way for the control to communicate with the container. For ActiveX controls, events are nothing more than IDispatch interfaces that are implemented on the container side of the container/control
relationship. The mechanism that events are based on is known as a connection point. A connection point is simply a description of the type of interface that is required in order to communicate with the container. Connection points are not restricted to only IDispatch interfaces, but rather they can be of any COM interface that is understood by both components. For that matter, connection points/events are not restricted only to controls, they can be used in any COM implementation. Controls were simply the first to take advantage of them. For more information regarding connection points, refer to the documentation in the OLE online help or to Kraig Brockschmidt's Inside OLE, Second Edition, from Microsoft Press. As with methods and properties, events are added and removed with the ClassWizard. Open the ClassWizard, and select the ActiveX Events tab. Select the CMFCControlWinCtrl class, and click the Add Event button. You can choose from a list of predefined events or add your own. You are going to add a Change event (see fig. 6.15), and it will have two parameters. The first parameter will be a string, passed by reference (BSTR *), called cstrCaption. The second will be a long, also passed by reference (long *), called lAlignment. You are passing the parameters by reference to allow the container application the opportunity to change the values if necessary. Click OK to add the event to the class. Before you use your event, you are going to create a common FireChange function that will encapsulate the code that deals with the cases where the program has changed the data passed to the event. Close the MFC ClassWizard dialog to confirm the addition of the event to the class. You need to add your function prototype to the class definition in the header file (see Listing 6.15). And you need to add the implementation to the source file (see Listing 6.16). FIG. 6.15 Here is where you add the Change event.
Listing 6.15 MFCCONTROLWINCTL.H--FireChange Function Prototype Added to the Class Definition . . . protected: // storage variable for the caption CString m_cstrCaption; // storage variable for the alignment long m_lAlignment; void FireChange(void); };
Listing 6.16 MFCCONTROLWINCTL.CPP--FireChange Implementation Added to the MFControlWinCtl.cpp Source File void CMFCControlWinCtrl::FireChange(void) { // get a BSTR that can be passed via the event BSTR bstrCaption = m_cstrCaption.AllocSysString(); // fire the change event this->FireChange(&bstrCaption, &m_lAlignment); // let's see what the user gave us m_cstrCaption = bstrCaption; // free the data that was passed back ::SysFreeString(bstrCaption); }
NOTE: The control is responsible for the data that is passed in to the event, so be sure to free the BSTR that was passed in when the function has returned to prevent a memory leak.
The common FireChange function allows you to hide the details surrounding the change event from the rest of the program. If you decide in the future to change its implementation, it will impact only one function rather than a number of them. The CaptionMethod will require that you fire a Change event if the data changes, so you will add your new event there (see Listing 6.17). You also want to add it to the SetAlignment function, but do not add FireChange to the SetCaptionProp function because it defers to the CaptionMethod function. Also, do not forget to add the function to any new functions or features that are added to the control as its implementation progresses.
Listing 6.17 MFCCONTROWINCTL.CPP--FireChange Event Added to the CaptionMethod Implementation . . . // if everything was OK if(TRUE == lResult) { // if we have a string if(lpctstrCaption != NULL) // assign the string to our member variable m_cstrCaption = lpctstrCaption; // did they pass us bad data? if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT) // sure did, lets fix their little red wagon m_lAlignment = EALIGN_LEFT; // fire the global change event this->FireChange(); // force the control to repaint itself this->InvalidateControl(); } // return the result of the function call return lResult; }
Persistence Persistence refers to the capability of a component to retain its state across execution lifetimes. In other words, regardless of the number of times that the control is started and stopped, it remembers that you changed its background color from white to mauve (even if it finds the color mauve revolting). The implementation of persistence, for MFC controls, is a no-brainer consisting of very little code. Simply add a single line to the DoPropExchange method of your control for each one of your properties, and you are finished. You can get a little more fancy and persist the properties differently, depending on whether you are saving or loading the properties, but that isn't real complicated either. Now modify your DoPropExchange function to persist the two properties, m_lAlignment and
m_cstrCaption. The persistence can be implemented two ways. The first, and the simplest, is to add one entry for each property without regard for order or dependencies. Dependencies refers to the fact that as your control implementation gets more complicated or requires a larger number of properties, you may find that how or when certain properties are persisted can depend on other properties.
TIP: When specifying the default value in your persistence entries, never use the same variable as the one that is going to receive the data. The MFC persistence functions are optimized so that if the value being persisted has not changed compared to the default value, it will not be persisted. This is to save time and space when loading or saving the persistence information. If you use the same variable, MFC will think that the value has never changed because the MFC persistence routines perform simple address and memory comparisons. Use constants for your default persistence information because that is what the routines expect.
Listing 6.18 demonstrates the simplest way to persist the properties. The same method is used for each property regardless of the direction of the persistence; when you are loading or saving, MFC handles all those details for you.
Listing 6.18 MFCCONTROLWINCTL.CPP--Simple DoPropExchange Implementation void CMFCControlWinCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); // TODO: Call PX_ functions for each persistent custom property. PX_Long(pPX, _T("Alignment"), m_lAlignment, EALIGN_LEFT); PX_String(pPX, _T("CaptionProp"), m_cstrCaption, _T("")); } Listing 6.19 demonstrates how you would implement your persistence if you wanted to load your properties first and then perform some implementation specific action--and conversely when saving the properties. The requirement to specifically order the persistence process is highly subjective and will depend greatly on your implementation. Listing 6.19 merely demonstrates the possibility of doing so.
Listing 6.19 MFCCONTROLWINCTL.CPP--DoPropExchange Implementation Separated into Distinct Operations void CMFCControlWinCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); // TODO: Call PX_ functions for each persistent custom property. // if we are loading the properties if(pPX->IsLoading()) { PX_Long(pPX, _T("Alignment"), m_lAlignment, EALIGN_LEFT); PX_String(pPX, _T("CaptionProp"), m_cstrCaption, _T("")); } // if we are saving the properties if(!pPX->IsLoading()) {
PX_Long(pPX, _T("Alignment"), m_lAlignment, EALIGN_LEFT); PX_String(pPX, _T("CaptionProp"), m_cstrCaption, _T("")); } }
TIP: If your control contains stock properties, you must call the base class implementation COleControl::DoPropExchange to support their persistence. The base class implementation does not allow much room for flexibility, though, and you may find yourself in more need of control than is allowed. One option is to persist the stock properties yourself and ignore the base class implementation. Listing 6.20 shows what code is necessary to persist the BackColor property and supply a different default value. In this case, the default value is the system defined WindowText color and not the ambient color of the container. There is a minor difference between MFC versions 4.1 and 4.2 in how the stock mask is retrieved, but other than that, the remainder of the code is copied verbatim from the MFC source files and has not been altered in any way. The one major drawback to this is that you are now required to maintain your stock property persistence code by hand and can no longer rely on the base class implementation provided by MFC.
Listing 6.20 EXAMPLE--Example Implementation of the Code Needed to Support Stock Property Persistence void CSOTABaseCtrl::DoPropExchange(CPropExchange* pPX) { // ****** Taken from "void COleControl::DoPropExchange(CPropExchange* pPX)" ctlcore.cpp // ** ASSERT_POINTER(pPX, CPropExchange); // ** // ****** Taken from "void COleControl::DoPropExchange(CPropExchange* pPX)" ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); // COleControl::DoPropExchange(pPX); // ****** Taken from "void COleControl::DoPropExchange(CPropExchange* pPX)" ctlcore.cpp // ** ExchangeExtent(pPX); // ** // ****** Taken from "void COleControl::DoPropExchange(CPropExchange* pPX)" ctlcore.cpp // ****** Taken from "void COleControl::ExchangeStockProps(CPropExchange* pPX)" ctlprop.cpp // ** BOOL bLoading = pPX->IsLoading(); // VC++ 4.2 DWORD dwPersistMask = GetStockPropMask(); // VC++ 4.1 // DWORD dwPersistMask = m_dwStockPropMask; PX_ULong(pPX, _T("_StockProps"), dwPersistMask); if (dwPersistMask & STOCKPROP_BACKCOLOR)
{ if (dwPersistMask & STOCKPROP_BACKCOLOR) PX_Color(pPX, _T("BackColor"), m_clrBackColor, 0x80000000 | COLOR_WINDOW /*AmbientBackColor()*/); else if (bLoading) m_clrBackColor = 0x80000000 | COLOR_WINDOW /*AmbientBackColor()*/ } // ** // ****** Taken from "void COleControl::ExchangeStockProps(CPropExchange* pPX)" ctlprop.cpp
Drawing the Control Most controls will have some form of UI. Although since the release of the OC 96 specification and ActiveX, that is no longer a requirement. Drawing the UI of a control has long been regarded as one of the most critical aspects of a control, in terms of both appearance and performance. A control that flashes or flickers a lot appears poorly developed regardless of its internal implementation. The same is true for how fast the control draws. With the advent of the Internet and ActiveX, it is even more crucial that a control draw fast and draw well. Drawing can be broken into two major categories: standard and optimized. In this chapter, you focus only on standard drawing. Chapter 7 introduces optimized drawing.
Standard Drawing Standard drawing is just that: standard. You have complete freedom to draw the control any way you see fit, using any method that is appropriate. You can use pens and brushes, rectangles and circles. Remember that drawing smart is the goal of any application with UI.
TIP: Probably the greatest sources of flicker and flash are overlapped painting and unnecessary drawing-drawing areas of the control that do not need to be drawn. Try to draw only to the areas of the control that have been invalidated. Doing so will save time and will prevent annoying flash. For example, if your control has a white background and a black border, draw only the white portion where it is going to be seen. Don't draw over the border as this causes the control to flash every time it gets a paint message.
Before adding the OnDraw implementation to your source file, add a CBrush member variable to the CMFCControlWinCtrl class (see Listing 6.21). The new member variable will hold a CBrush object and will be used more fully in the optimized drawing section you find in Chapter 7. For now, the CBrush object will be re-created every time the OnDraw function executes.
Listing 6.21 MFCCONTROLWINCTL.H--CBrush Member Variable Added to CMFCControlWinCtrl protected: // storage variable for the caption CString m_cstrCaption; // storage variable for the alignment long m_lAlignment;
void FireChange(void); // brush CBrush m_Brush; }; Listing 6.22 contains the code for drawing the control. The OnDraw implementation is fairly straightforward. Select the font into the DC, get all of the colors that you are going to use, draw the background, draw the text, and draw the border. Then reset everything back to the way it was when you started.
Listing 6.22 MFCCONTROLWINCTL.CPP--Standard Drawing Added to the OnDraw Function void CMFCControlWinCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { // ****** Get the text font ****** // ** CFont * pOldFont; // get the stock font pOldFont = this->SelectStockFont(pdc); // ** // ****** Get the text font ****** // ****** Get the colors ****** // ** // use the window color as the background color COLORREF clrTextBackgroundColor = this->TranslateColor(this- >GetBackColor()); // then use the normal windows color for the text COLORREF clrTextForegroundColor = this->TranslateColor(this- >GetForeColor()); // set to the system color COLORREF clrEdgeBackgroundColor = ::GetSysColor(COLOR_3DFACE); COLORREF clrEdgeForegroundColor = ::GetSysColor(COLOR_3DFACE); // ** // ****** Get the colors ****** // ****** Draw the background ****** // ** // set the text color COLORREF clrOldBackgroundColor = pdc->SetBkColor(clrTextBackgroundColor); COLORREF clrOldForegroundColor = pdc >SetTextColor(clrTextForegroundColor); // if we don't have a brush if(m_Brush.m_hObject == NULL) // create a solid brush m_Brush.CreateSolidBrush(clrTextBackgroundColor); // select the brush and save the old one CBrush * pOldBrush = pdc->SelectObject(&m_Brush); // draw the background pdc->Rectangle(&rcBounds); // ** // ****** Draw the background ****** // ****** Draw the text ****** // ** int iHor, iVer; // get the size of the text for this DC
CSize oSize = pdc->GetTextExtent(m_cstrCaption); switch(m_lAlignment) { case EALIGN_CENTER: iHor = (rcBounds.right - oSize.cx) / 2; iVer = rcBounds.top + 3; break; case EALIGN_RIGHT: iHor = rcBounds.right - oSize.cx - 3; iVer = rcBounds.top + 3; break; // case EALIGN_LEFT: default: iHor = rcBounds.left + 3; iVer = rcBounds.top + 3; break; } // output our text pdc->ExtTextOut(iHor, iVer, ETO_CLIPPED | ETO_OPAQUE, rcBounds, m_cstrCaption, m_cstrCaption.GetLength(), NULL); // ** // ****** Draw the text ****** // ****** Draw the border ****** // ** // set the edge style and flags UINT uiBorderStyle = EDGE_SUNKEN; UINT uiBorderFlags = BF_RECT; // set the edge color pdc->SetBkColor(clrEdgeBackgroundColor); pdc->SetTextColor(clrEdgeForegroundColor); // draw the 3D edge pdc->DrawEdge((LPRECT)(LPCRECT) rcBounds, uiBorderStyle, uiBorderFlags); // ** // ****** Draw the border ****** // ****** Reset the colors ****** // ** // restore the original colors pdc->SetBkColor(clrOldBackgroundColor); pdc->SetTextColor(clrOldForegroundColor); // ** // ****** Reset the colors ****** // ****** release the text font ****** // ** // set the old font back pdc->SelectObject(pOldFont); // ** // ****** Get the text font ****** // select the old brush back pdc->SelectObject(pOldBrush); }
From Here...
This chapter focused on creating a basic MFC control implementation. Methods, properties, and events are the basis for any control implementation, and using MFC makes it truly easy to add them in a short period of time. This chapter also addressed the issues of persistence and drawing, which are necessary for a complete control implementation. MFC was the first tool available from Microsoft for doing ActiveX development--back then it was called OLE. MFC was meant to hide all the details of implementing ActiveX/COM components from you so that you wouldn't have to learn a whole new way of developing MFC applications just to support ActiveX/COM. The fact that MFC hides all of the details is exactly its limitation. You may be able to create ActiveX controls in a short period of time, but their size and performance may not be that desirable. MFC does have a distinct advantage in the area of maturity of integration with the IDE since the IDE seems to be designed around making it as easy as possible to create applications when using MFC. Chapter 7 expands on the things you learned in this chapter about creating controls. It also shows you how to add new features to your control implementation that can make it truly unique and interesting.
Chapter 7 Advanced ActiveX Control Development with MFC ●
Advanced ActiveX Control Development with MFC ❍ Properties ■ Creating Asynchronous Properties ■ Listing 7.1 MFCONTROLWINCTL.CPP--Constructor Initialization of ReadyState ■ Listing 7.2 MYDATAPATH.H--CString Member Variable Added to the CMyDataPath Class ■ Listing 7.3 MFCCONTROWINCTL.H--CMFCControlWinCtrl Class Updated to Include the CMyDataPath Class ■ Listing 7.4 MYDATAPATH.CPP--OnDataAvailable Implementation ■ Listing 7.5 MFCCONTROLWINCTL.CPP--oMyDataPath Object Initialized in the CMFCControlWinCtrl Class Constructor ■ Listing 7.6 MFCCONTROLWINCTRL.CPP--oMyDataPath Added to DoPropExchange ■ Listing 7.7 MFCCONTROLWINCTL.H--Get/Set TextData Property Implementation ■ Static and Dynamic Property Enumeration ■ Listing 7.8 MFCCONTROL.ODL--EALIGNMENT Enumeration Added to the MFCControl.odl File ■ Listing 7.9 MFCCONTROLWINCTL.H--Dynamic Property Enumeration Function Prototypes Added to the CMFCControlWinCtrl Class ■ Listing 7.10 MFCCONTROLWINCTL.CPP--OnGetPredefinedStrings Implementation ■ Listing 7.11 MFCCONTROLWINCTL.H--Control dispid Enumeration ■ Listing 7.12 MFCCONTROLWINCTL.CPP-- OnGetPredefinedValue Implementation ■ Listing 7.13 MFCCONTROLWINCTL.CPP--OnGetDisplayString Implementation ❍ Drawing the Control ■ Listing 7.14 MFCCONTROWINCTL.CPP--Optimized OnDraw Function ❍ Adding Clipboard and Drag and Drop Support ■ Clipboard Support ■ Listing 7.15 MFCCONTROLWINCTL.CPP--OnKeyDown Implementation ■ Listing 7.16 MFCCONTROLWINCTL.H--Clipboard Source Support Helper Function Prototypes ■ Listing 7.17 MFCCONTROLWINCTL.CPP--CopyDataToClipboard Implementation ■ Listing 7.18 MFCCONTROLWINCTL.CPP-- PrepareDataForTransfer Implementation ■ Listing 7.19 MFCCONTROLWINCTL.H--Clipboard Target Support Helper Function Prototypes ■ Listing 7.20 MFCCONTROLWINCTL.CPP--GetDataFromClipboard Implementation ■ Listing 7.21 MFCCONTROLWINCTL.CPP--GetDataFromTransfer Implementation ■ Drag and Drop Support ■ Listing 7.22 MFCCONTROLWINCTL.CPP--OnLButtonDown Implementation ■ Listing 7.23 MFCCONTROLWINCTL.H--CMyOleDropTarget Class Declaration Added to the CMFCControlWinCtl Class ■ Listing 7.24 MFCCONTROLWINCTL.CPP--OnCreate Implementation ■ Listing 7.25 MFCCONTROLWINCTL.CPP--OnDragOver Implementation
Listing 7.26 MFCCONTROLWINCTL.CPP--OnDrop Implementation ■ Custom Clipboard and Drag and Drop Formats ■ Listing 7.27 MFCCONTROLWINCTL.H--m_uiCustomFormat Member Variable Added to the CMFCControlWinCtrl Class ■ Listing 7.28 MFCCONTROLWINCTL.CPP--Custom Clipboard Format Registered in the CMFCControlWinCtrl Constructor ■ Listing 7.29 MFCCONTROLWINCTL.CPP--PrepareDataForTransfer Function Updated to Support Custom Clipboard Formats ■ Listing 7.30 MFCCONTROLWINCTL.CPP--GetDataFromTransfer Function Updated to Support Custom Clipboard Formats Subclassing Existing Windows Controls ■ Listing 7.31 MFCCONTROLSUBWINCTL.CPP--Additional Code Requirements for ActiveX Controls that Subclass Existing Windows Controls Dual-Interface Controls Other ActiveX Features ■ Windowless Activation ■
❍
❍ ❍
■
Flicker-Free Activation ■ Unclipped Device Context ■ Mouse Pointer Notifications When Inactive From Here... ■
❍
Advanced ActiveX Control Development with MFC ●
●
●
●
●
Adding asynchronous properties MFC hides many details of support of asychronous properties, allowing you to focus on your control implementation. Optimized drawing Optimized drawing with MFC is easy and can enhance the overall performance of the control. Clipboard and Drag and Drop support Using MFC to add Clipboard and Drag and Drop support to mundane control implementations can have profound effects. Subclassing Windows controls Subclassing an existing Windows control can reduce your development time when creating new controls, and MFC can be a hinderance. Dual-interface controls MFC does not support creation of dual-interface controls by default, but you can add them.
This chapter expands upon the information in Chapter 6 about creating a basic MFC ActiveX control. In addition
to the features that you are familiar with, such as Clipboard and Drag and Drop support, you will learn how to implement asynchronous properties and optimized drawings, which are the result of the adoption of OC 96 specification.
Properties Chapter 6 tells you how to add the various types of properties to your control implementation. One type of property has yet to be examined: asynchronous properties.
Creating Asynchronous Properties Asynchronous properties are those properties that typically represent a large amount of data, such as a text file or a bitmap, and are loaded as a background process so as not to interfere with the normal processing of the control and the container. This statement can be somewhat misleading. Asynchronous refers only to the call to load the data; it does not refer to the actual loading. For example, a control uses a bitmap as its background and has defined it as an asynchronous property. If OLE determines that the bitmap is already on the local machine, the data is considered to be available to the control and, subsequently, will instruct the control that all of the data is available. If OLE determines that the bitmap is not available on the local machine, OLE will load the data as fast as possible and inform the control as data becomes available. After the data is in a location that is considered accessible, the property essentially behaves as any other property would. If you require the asynchronous loading of the data regardless of its location, you must implement it yourself. Except for a few changes, asynchronous properties are added in the same fashion as any other property. In Chapter 6, when you initially create your control, you have the opportunity to define some ActiveX features, one being Loads Properties Asynchronously. Choosing this option adds some code to your application that normally would not be implemented. First, the stock property ReadyState was added to your control. This property is used to notify the container of the state that the control is in while loading its properties. The stock event ReadyStateChange, which is used to notify the container that the ReadyState of the control has changed, was also added. The last thing that was added was the initialization of the member variable m_lReadyState to READYSTATE_LOADING in the controls constructor (see Listing 7.1).
Listing 7.1 MFCONTROLWINCTL.CPP--Constructor Initialization of ReadyState CMFCControlWinCtrl::CMFCControlWinCtrl() { InitializeIIDs(&IID_DMFCControlWin, &IID_DMFCControlWinEvents); m_lReadyState = READYSTATE_LOADING; // TODO: Call InternalSetReadyState when the readystate changes. // set the alignment to the default of left m_lAlignment = EALIGN_LEFT; } This is the extent of the work that is done for you at the time your project is created. The control must have an entry point for OLE to communicate with when notifying the control when and how much data is available. This is accomplished through the CDataPathProperty or the
CCachedDataPathProperty class. CDataPathProperty is used for data that typically arrives in a continuous fashion, such as stock market data. CCachedDataPathProperty is used for data that is retrieved once and then stored or cached, such as a file. You must implement a class in your control that is inherited from one of these classes. To add a new class, you use the ClassWizard. For the purposes of this chapter and the sample application, you will implement an asynchronous property that reads string data from a file and outputs the data as the caption of the control. Open the ClassWizard, click the Add Class button on any one of the tab pages, and select the New menu item. In the New Class dialog (see fig. 7.1), enter the Name CMyDataPath, select a Base class of CCachedDataPathProperty, and click the OK button to add the new class. FIG. 7.1 Add a new CCachedDataPath Property class with the ClassWizard. Your control must override the OnDataAvailable function within the CMyDataPath class in order to receive data as it becomes available. From the open ClassWizard dialog, select the Message Maps tab and locate the OnDataAvailable message within the Messa_ges list box. Double-click the OnDataAvailable message to add the method to your class. Click the OK button to close the ClassWizard. The first step is to add the cstrMyBuffer member variable to the CMyDataPath class (see Listing 7.2). The member variable, cstrMyBuffer, is used to store all of the data as it is passed to the OnDataAvailable function.
Listing 7.2 MYDATAPATH.H--CString Member Variable Added to the CMyDataPath Class . . . // Implementation protected: CString cstrMyBuffer; }; The next step is to update the control class, CMFCControlWinCtrl, to include the header file of the CMyDataPath class and also to add a new member variable, oMyDataPath (see Listing 7.3). The CMyDataPath class also needs access to the function CaptionMethod in the control so that the data can be placed in the control when it is all available. Since the CaptionMethod is a protected function, it is necessary to allow the CMyDataPath class access to the function using the C++ friend declaration.
Listing 7.3 MFCCONTROWINCTL.H--CMFCControlWinCtrl Class Updated to Include the CMyDataPath Class . . . #include "alignmentenums.h" #include "mydatapath.h" ///////////////////////////////////////////////////////////////////////////// // CMFCControlWinCtrl : See MFCControlWinCtl.cpp for implementation. . . .
friend class CMyDataPath; CMyDataPath oMyDataPath; }; . . . The next step is to add the code to the OnDataAvailable function in the CMyDataPath class to deal with the data as it is sent to the control. The OnDataAvailable method is straightforward (see Listing 7.4). It has two properties, dwSize and bscfFlag. DwSize is the number of bytes of data that are currently available in this call to the function. BscfFlag is a set of flags indicating the current operation taking place (see Table 7.1). Table 7.1 BSCF Notifications Notification Message
Description
BSCF_FIRSTDATANOTIFICATION
This message is sent the first time that the OnDataAvailable function is called. It is important to note that this message can be sent along with the BSCF_LASTDATANOTIFICATION message.
BSCF_INTERMEDIATEDATANOTIFICATION This message is sent while the OLE is still loading data. BSCF_LASTDATANOTIFICATION
This message is sent when a control has been given all of the data that is available to the property. It is important to note that this message can be sent along with the BSCF_FIRSTDATANOTIFICATION message.
Note the use of the BSCF notification messages and how you must respond to each. Do not process the messages exclusive of each other; you must process each message individually. Remember that you can receive more than one notification message in each call to OnDataAvailable. The OnDataAvailable implementation first clears the string buffer if this is the first call to the function. Next it creates a buffer of the appropriate size to receive the data from the Read function. If the data was successfully read, the data is added to the member variable cstrMyBuffer. Finally if this is the last call to the OnDataAvailable function, the CaptionMethod is called supplying the new data and an empty variant parameter for the alignment. The last thing you must do is change the state of the control to READYSTATE_COMPLETE, which indicates that all of the asynchronous properties have been loaded.
Listing 7.4 MYDATAPATH.CPP--OnDataAvailable Implementation void CMyDataPath::OnDataAvailable(DWORD dwSize, DWORD bscfFlag) { // if this is the first notification if(bscfFlag & BSCF_FIRSTDATANOTIFICATION) // clear the string buffer cstrMyBuffer.Empty(); CString cstrTempBuffer; // get a temp buffer LPTSTR lptstrTempBuffer = cstrTempBuffer.GetBuffer(dwSize); // read the data to a temp buffer UINT uiBytesRead = this->Read(lptstrTempBuffer, dwSize); // if we read in any data if(uiBytesRead)
{ // store the data cstrTempBuffer.ReleaseBuffer(uiBytesRead); cstrMyBuffer += cstrTempBuffer; } // if this is our last notification if(bscfFlag & BSCF_LASTDATANOTIFICATION) { VARIANT varAlignment; ::VariantInit(&varAlignment); varAlignment.vt = VT_EMPTY; ((CMFCControlWinCtrl *) this->GetControl())->CaptionMethod(cstrMyBuffer, varAlignment); this->GetControl()->InternalSetReadyState(READYSTATE_COMPLETE); } } Next you need to add an OLE property to store the actual location of the data that is to be loaded asynchronously. Open the ClassWizard, select the Automation tab, select the CMFCControlWinCtrl class, and click the Add Property button. In the Add Property dialog, enter the External name TextData, select the Type BSTR and an Implementation of Get/Set methods. Do not use the OLE_DATAPATH type for this property, as it doesn't work; the type must be a BSTR. Use of the OLE_DATAPATH and its related problems is a known bug with Microsoft and MFC. Click the OK button to close the dialog and add the property to the class. Double-click the TextData entry in the External names list box to close the ClassWizard and open the source file. To complete your implementation, you need to add some code to the CMFCControlWinCtrl class source file. First update the class constructor to set the control class in the oMyDataPath object (see Listing 7.5). Doing this is absolutely necessary for the DoPropExchange function to work correctly when loading the data path property.
Listing 7.5 MFCCONTROLWINCTL.CPP--oMyDataPath Object Initialized in the CMFCControlWinCtrl Class Constructor CMFCControlWinCtrl::CMFCControlWinCtrl() { InitializeIIDs(&IID_DMFCControlWin, &IID_DMFCControlWinEvents); // TODO: Call InternalSetReadyState when the readystate changes. m_lReadyState = READYSTATE_LOADING; // set the alignment to the default of left m_lAlignment = EALIGN_LEFT; // don't forget this - DoPropExchange won't work without it oMyDataPath.SetControl(this); } Next add the oMyDataPath property persistence to the DoPropExchange function (see Listing 7.6). Remember that without the call to SetControl in the constructor, this code will not function correctly.
Listing 7.6 MFCCONTROLWINCTRL.CPP--oMyDataPath Added to DoPropExchange
void CMFCControlWinCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); // TODO: Call PX_ functions for each persistent custom property. // if we are loading the properties if(pPX->IsLoading()) { PX_Long(pPX, _T("Alignment"), m_lAlignment, EALIGN_LEFT); PX_String(pPX, _T("CaptionProp"), m_cstrCaption, _T("")); PX_DataPath(pPX, _T("TextData"), oMyDataPath); } // if we are saving the properties if(!pPX->IsLoading()) { PX_Long(pPX, _T("Alignment"), m_lAlignment, EALIGN_LEFT); PX_String(pPX, _T("CaptionProp"), m_cstrCaption, _T("")); PX_DataPath(pPX, _T("TextData"), oMyDataPath); } } Finally you add the code to the GetTextData and SetTextData methods (see Listing 7.7). The GetTextData function simply returns a UNICODE version of the property value. The SetTextData implementation calls the Load function to load the data pointed to by the lpszNewValue parameter. After the Load function returns, the SetModifiedFlag and InvalidateControl functions are called to update the control and the data with the new information.
Listing 7.7 MFCCONTROLWINCTL.H--Get/Set TextData Property Implementation BSTR CMFCControlWinCtrl::GetTextData() { // retrieve the path and allocate a BSTR from it return (oMyDataPath.GetPath()).AllocSysString(); } void CMFCControlWinCtrl::SetTextData(LPCTSTR lpszNewValue) { // load the DataPath variable based on the new information this->Load(lpszNewValue, oMyDataPath); // update the property this->SetModifiedFlag(); // redraw the control this->InvalidateControl(); } Again, it is very important to understand that asynchronous properties are not a guarantee of improved
performance in a general sense. They are merely an option available to you for creating Internet controls that are more responsive across slower network connections.
Static and Dynamic Property Enumeration Property enumeration is a way of restricting a property to a specific set of valid values. An example of an enumeration is a property for determining the alignment of a control's displayed text: left-justified, centered, and right-justified, in your case. Another case is a property used to select the different languages a control supports. This is a good candidate for both a static set, say English and German, and a dynamic set, say for all the languages on a particular machine. Adding property enumeration greatly enhances the look and feel of your control. This addition gives the user all the options that are possible, with a simple click of a mouse. As a result the user doesn't have to rather than trying to search through documentation or, worse yet, trying to guess the acceptable values. When editing the value of an enumerated property within a property browser, note that development tools such as Visual Basic display all the values of the enumeration using a string representation rather than just the actual value that the control can accept. For a Boolean data type, the strings used are TRUE and FALSE, representing -1 or 0, respectively. Two approaches can be taken when creating an enumeration for a property: You can use a static approach, with an enumeration defined in the control's ODL file, or a dynamic approach, with enumeration code implemented in the control itself. Static Property Enumeration ODL allows for the creation of C/C++ style enumeration declarations that conform to the same rules as C/C++. Like C/C++, the new enumeration can be used as data type within the ODL file. While this style of enumeration is by far the easiest, it is also the most restrictive because the enumeration is static to the type library. The Alignment property is a good candidate for an enumeration because internally that is how it is validated and used. Listing 7.8 contains the code that was added to your ODL file to support the Alignment enumeration. Remember to generate a new UUID for the enumeration with the GUIDGEN.EXE application included with VC++. The helpstring is what the user will see within the property browser of your development environment. If you leave off the helpstring, the actual numeric value will appear instead. The last thing you did was to change the data type of the Alignment property from long to EALIGNMENT. This is required if the property is to display the enumerated values within the property browser.
NOTE: ODL is very flexible in that it allows much the same style of data type declaration and use as that of C or C++. Any data type that can be declared in ODL can also be referenced and used within the same and other ODL files, including interface declarations. In addition, other type libraries can be imported into an ODL file to provide access to other user-defined data types. In the case of the sample control, two types of libraries, STDOLE_TLB and STDTYPE_TLB, are included for the standard OLE interface and data type declarations. The ODL documentation can be a little difficult to decipher, but we recommend that you at least review the documentation. It is well worth the effort just to see what you can and cannot do with your type library and how it will affect the container of your control or component.
It is absolutely critical that the developer of containers adhere to the standards established in the ODL documentation. You will find that some of the aspects of type library creation and use are based on cooperation and trust and that component developers depend on that.
Listing 7.8 MFCCONTROL.ODL--EALIGNMENT Enumeration Added to the MFCControl.odl File typedef [ uuid(7F369B90-380D-11d0-BCB6-0020AFD6738C) ] enum tagAlignmentEnum { [helpstring("Left Justify")] EALIGN_LEFT = 0, [helpstring("Right Justify")] EALIGN_RIGHT = 1, [helpstring("Center")] EALIGN_CENTER = 2, }EALIGNMENT; // Primary dispatch interface for CMFCControlWinCtrl [ uuid(14DD5C04-60DE-11D0-BEE9-00400538977D), helpstring("Dispatch interface for MFCControlWin Control"), hidden ] dispinterface _DMFCControlWin { properties: // NOTE - ClassWizard will maintain property information here. // Use extreme caution when editing this section. //{{AFX_ODL_PROP(CMFCControlWinCtrl) [id(DISPID_READYSTATE), readonly] long ReadyState; [id(1)] EALIGNMENT Alignment; [id(DISPID_BACKCOLOR), bindable, requestedit] OLE_COLOR BackColor; [id(2)] BSTR TextData; //}}AFX_ODL_PROP methods: . . . Dynamic Property Enumeration Dynamic property enumeration is the most flexible of the two types of property enumeration. It requires only a little more work on the part of the developer, and is worth the effort. Dynamic enumeration is perfect for situations where you need to restrict the data that can be entered into a property but are unable to determine until runtime what the valid values are. For example, a LocaleID property can enumerate all of the available LocaleIDs on the machine that the application is running on. Another property could enumerate the key field of a SQL database table. The possibilities are limitless and will provide your control a look and feel that goes far beyond the effort that was required to enable it. Dynamic property enumeration using MFC requires the implementation of three methods within your control: OnGetPredefinedStrings, OnGetPredefinedValue, and OnGetDisplayString, all of which are virtual functions declared in the COleControl class. Listing 7.9 shows the function prototypes that need to be added to the control's class declaration.
Listing 7.9 MFCCONTROLWINCTL.H--Dynamic Property Enumeration Function Prototypes Added to the CMFCControlWinCtrl Class
class CMFCControlWinCtrl : public COleControl { DECLARE_DYNCREATE(CMFCControlWinCtrl) // Constructor public: CMFCControlWinCtrl(); BOOL OnGetPredefinedStrings(DISPID dispid, CStringArray* pStringArray, CDWordArray* pCookieArray); BOOL OnGetPredefinedValue(DISPID dispid, DWORD dwCookie, VARIANT FAR* lpvarOut); BOOL OnGetDisplayString( DISPID dispid, CString& strValue ); // Overrides . . . OnGetPredefinedStrings is called by the container application when it needs to load the strings and cookies for the enumeration (see Listing 7.10). The string is used as the text representation of the enumerated value to make it more meaningful to the user. The cookie is a 32-bit value that can be used in any way that is appropriate to identify the value associated with the display string of the enumeration. In your case, you are going to store the actual value that the string represents, but it could just as easily have been a pointer or index into some form of storage.
Listing 7.10 MFCCONTROLWINCTL.CPP--OnGetPredefinedStrings Implementation BOOL CMFCControlWinCtrl::OnGetPredefinedStrings(DISPID dispid, CStringArray* pStringArray, CDWordArray* pCookieArray) { BOOL bResult = FALSE; // which property is it switch(dispid) { case dispidAlignment: { // add the string to the array pStringArray->Add(_T("Left Justify")); // add the value to the array pCookieArray->Add(EALIGN_LEFT); // add the string to the array pStringArray->Add(_T("Center")); // add the value to the array pCookieArray->Add(EALIGN_CENTER); // add the string to the array pStringArray->Add(_T("Right Justify")); // add the value to the array pCookieArray->Add(EALIGN_RIGHT);
// set the return value bResult = TRUE; } break; } return bResult; } The OnGetPredefinedStrings function is called for every one of the properties in your control, so your code will have to check the dispid that is passed to the function to make sure that the correct enumeration is going to the correct property. The dispids of the control properties can be found in the class declaration of your control and are maintained automatically by the ClassWizard (see Listing 7.11).
Listing 7.11 MFCCONTROLWINCTL.H--Control dispid Enumeration . . . // Dispatch and event IDs public: enum { //{{AFX_DISP_ID(CMFCControlWinCtrl) dispidAlignment = 1L, dispidTextData = 2L, dispidCaptionMethod = 3L, dispidCaptionProp = 4L, eventidChange = 1L, //}}AFX_DISP_ID }; . . . When the user selects one of the enumerated values in the property browser, the control's OnGetPredefinedValue function is fired (see Listing 7.12). This allows the container to retrieve the actual value that should be stored in the property in place of the string representation. The function is passed the dispid identifying the property that is being changed and the cookie value that was assigned to the string representation in the OnGetPredefinedStrings function.
Listing 7.12 MFCCONTROLWINCTL.CPP-- OnGetPredefinedValue Implementation BOOL CMFCControlWinCtrl::OnGetPredefinedValue(DISPID dispid, DWORD dwCookie, VARIANT FAR* lpvarOut) { BOOL bResult = FALSE; // which property is it switch(dispid) { case dispidAlignment: // clear the variant
::VariantInit(lpvarOut); // set the type to a long lpvarOut->vt = VT_I4; // set the value to the value that was stored with the string lpvarOut->lVal = dwCookie; // set the return value bResult = TRUE; break; } return bResult; } OnGetDisplayString (see Listing 7.13) is the last method to be called and is required if the programmer wants the text representation to appear next to the property in the property browser when the property is not being edited. The function is passed a dispid, again reflecting the current property, and a CString object reference. The control should place the string, as reflected by the current state of the property, into the CString object and exit the function.
Listing 7.13 MFCCONTROLWINCTL.CPP--OnGetDisplayString Implementation BOOL CMFCControlWinCtrl::OnGetDisplayString( DISPID dispid, CString& strValue ) { BOOL bResult = FALSE; // which property is it switch(dispid) { case dispidAlignment: { switch(m_lAlignment) { case EALIGN_LEFT: strValue = _T("Left Justify"); break; case EALIGN_CENTER: strValue = _T("Center"); break; case EALIGN_RIGHT: strValue = _T("Right Justify"); break; } // set the return value bResult = TRUE; } break; } return bResult;
}
Drawing the Control Optimized drawing allows you to create drawing objects, such as pens or brushes, and rather than removing them when you are finished drawing, you can store them in your control member variables and use them again the next time your control draws itself. The benefit is that you create a pen once for the drawing lifetime of your control, instead of every time it draws. One thing to remember, though: Optimized drawing does not guarantee performance improvements. It simply supplies a framework for how drawing should be performed and how drawing resources should be used. A poorly written control is still poorly written, no matter how you slice it. Standard and optimized drawings have a single tradeoff, and that is size versus speed. Standard drawing does not require member variables for the drawing objects that are created and used--thus requiring less instance data but more processing time--whereas optimized code will be the opposite. An additional drawback to optimized drawing is that a container may not support it. A control must, at the very least, support standard drawing functionality, deferring to optimized only if appropriate. For MFC, the scope of optimized drawing is very narrow compared to the OC 96 specification, but it will, nonetheless, result in performance improvements if taken advantage of. The OC 96 specification further breaks optimized drawing into what is known as aspects, but MFC is not designed to allow that kind of drawing. For more information on aspect drawing, please see the OLE Control 96 Specification that ships with the ActiveX SDK. Optimized Drawing In Chapter 6, you learn how to implement standard drawing. In this chapter, you will enhance the original implementation to take advantage of drawing optimization. Listing 7.14 reflects the change that is made to the original drawing code to use optimization. That's it. You just check to see whether the container supports optimized drawing. If the container supports optimized drawing, you leave the CBrush object alone and use it again when you have to draw the control's UI. Thus, you save time every time you redraw the UI because you load the brush only once. When the control is destroyed, the CBrush object will go out of scope and will remove the brush object for you. Optimized drawing is that simple, but it depends on the container application for support.
Listing 7.14 MFCCONTROWINCTL.CPP--Optimized OnDraw Function . . . // set the old font back pdc->SelectObject(pOldFont); // ** // ****** Get the text font ****** // The container does not support optimized drawing. if(!IsOptimizedDraw()) { // select the old brush back pdc->SelectObject(pOldBrush); } }
Adding Clipboard and Drag and Drop Support
The Clipboard is an area of the Windows operating system that acts like a bulletin board for data that can be shared between applications. By means of a series of keystrokes or menu options, a user can copy data to the Clipboard from one application and paste the data from the Clipboard into another application. Drag and Drop provides the user with the ability to transfer data between two applications by means of a mouse only. The user selects the data to transfer, holds down a mouse button on the selected data, drags the data to the location where is it to be added, and releases the mouse button, thus dropping the data into the new location. Drag and Drop support is similar in its implementation to Clipboard support, and you will take full advantage of it in your implementa Adding Clipboard and Drag and Drop support to a control can make even the simplest implementations appear more professional and well-rounded.
Clipboard Support The first step is deciding which keystrokes will be used to initiate the cut, copy, or paste operation. Fortunately, the Windows operating system already has a number of standards in this area. You will use Ctrl+X or Shift+Delete for Cut, Ctrl+C or Ctrl+Insert for Copy, and Ctrl+V or Shift+Insert for Paste. Open the ClassWizard and select the Message Maps tab for the CMFCControlWinCtrl class. Double-click WM_KEYDOWN in the list of messages to add the OnKeyDown function to your message map (see fig. 7.2). The OnKeyDown function is where you are going to add your code to look for the keystroke combinations that will initiate the Clipboard transfers. Double-click the OnKeyDown member function to close the ClassWizard and open the source file for editing. FIG. 7.2 Add the OnKeyDown message map with the ClassWizard. Listing 7.15 shows the code that is added to OnKeyDown to support the keystroke combinations listed in the preceding paragraph. The implementation is simple; based on the particular state of the Ctrl or Shift keys and the correct keystroke, the function either copies the data to or copies the data from the Clipboard.
Listing 7.15 MFCCONTROLWINCTL.CPP--OnKeyDown Implementation void CMFCControlWinCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { BOOL bHandled = FALSE; // find out if short sShift = // find out if short sControl
the shift key is being held down ::GetKeyState(VK_SHIFT); the control key is being held down = ::GetKeyState(VK_CONTROL);
switch(nChar) { case 0x56: // `V' // PASTE case 0x76: // `v' // if the control key is being held down if(sControl & 0x8000) {
// get any text from the clipboard this->GetDataFromClipboard(); // force the control to redraw itself this->InvalidateControl(NULL); // we don't need to pass this key to the base implementation bHandled = TRUE; } case 0x43: // `C' // COPY or PASTE case 0x63: // `c' case VK_INSERT: // if the control key is being held down if(sControl & 0x8000) { // copy the data to the clipboard this->CopyDataToClipboard(); // we don't need to pass this key to the base implementation bHandled = TRUE; } // if the shift key is being held down it is a PASTE else if(sShift & 0x8000 && nChar == VK_INSERT) { // get any text from the clipboard this->GetDataFromClipboard(); // force the control to redraw itself this->InvalidateControl(NULL); // we don't need to pass this key to the base implementation bHandled = TRUE; } break; case 0x58: // `X' // CUT case 0x78: // `x' case VK_DELETE: // if this is a shift delete OR CTRL-X/x if((nChar == VK_DELETE && (sShift & 0x8000)) || ((nChar == 0x58 || nChar == 0x78) && (sControl & 0x8000))) { this->CopyDataToClipboard(); // clear the string since this is a CUT operation m_cstrCaption.Empty(); // fire the global change event this->FireChange(); // force the control to repaint itself this->InvalidateControl(NULL); // we don't need to pass this key to the base implementation bHandled = TRUE;
} break; } // if we didn't handle the character if(!bHandled) { // and the control key is not being held down if(!(sControl & 0x8000)) // send to the default handler COleControl::OnKeyDown(nChar, nRepCnt, nFlags); } } In addition to the code that you added to trap the keystrokes, you also need to add four methods, which you will examine in detail in the next section, for dealing with the Clipboard transfers. CopyDataToClipboard will, as the name implies, get the data from the control, and using the helper function, PrepareDataForTransfer, will package the data and put it on the Clipboard. GetDataFromClipboard will open the Clipboard and look for data formats that the control understands. Upon finding a suitable format, GetDataFromClipboard will use the helper function GetDataFromTransfer to store the data in the control. The fact that the data transfer functions have been separated into two separate methods for each type of transfer, to and from the Clipboard, and then further broken down in each type of transfer into two separate steps will aid you when you enable the control for Drag and Drop support. This is because the basic data transfer mechanism is the same between the Clipboard and Drag and Drop and will allow you to rely on a large portion of shared code for each implementation. Using Built-In Clipboard Formats The Windows operating system defines a number of built-in data transfer formats for use with the Clipboard. You see the formats in the following list: CF_TEXT CF_BITMAP CF_METAFILEPICT CF_SYLK CF_DIF CF_TIFF CF_OEMTEXT CF_DIB CF_PALETTE CF_PENDATA CF_RIFF CF_WAVE CF_UNICODETEXT CF_ENHMETAFILE CF_HDROP CF_LOCALE CF_MAX CF_OWNERDISPLAY CF_DSPTEXT CF_DSPBITMAP CF_DSPMETAFILEPICT CF_DSPENHMETAFILE
CF_GDIOBJFIRST CF_GDIOBJLAST The first implementation of Clipboard data transfer will rely on the CF_TEXT format. This is a general format for transferring non-UNICODE text data. There are two aspects to using the Clipboard: being a Clipboard source and being a Clipboard target. Being a Clipboard source refers to an application's capability to copy data to the Clipboard. Being a Clipboard target refers to an application's capability to copy data from the Clipboard. You will first learn how to enable a control as a Clipboard source and then how to enable a control as a Clipboard target. Enabling a Control as a Clipboard Source A Clipboard source is an application that puts data on the Clipboard for other applications to copy. An application must support two COM interfaces, IDataObject and IEnumFORMATETC, in order to qualify as a valid Clipboard source. MFC provides the classes COleDataSource and COleDataObject, which perform all of the work of implementing the interfaces for you. The OnKeyDown implementation takes advantage of two helper functions, CopyDataToClipboard and PrepareDataForTransfer, when copying data to the Clipboard. First you need to add the two function prototypes to the CMFCControlWinCtrl class (see Listing 7.16).
Listing 7.16 MFCCONTROLWINCTL.H--Clipboard Source Support Helper Function Prototypes . . . void CopyDataToClipboard(void); void PrepareDataForTransfer(COleDataSource * opOleDataSource); }; . . . CopyDataToClipboard uses the COleDataSource class provided by MFC to connect to the Clipboard (see Listing 7.17). By calling the GetClipboardOwner function, your implementation first checks to see whether you already have an object on the Clipboard. GetClipboardOwner returns a pointer to a COleDataSource object if the Clipboard contains a COleDataSource object that you had previously set to the Clipboard using the SetClipboard function. If you are not the owner of the Clipboard, the method returns NULL. If you didn't get a reference to a COleDataSource object, you need to create one. Next you call the general method PrepareDataForTransfer passing in your COleDataSource object, which will copy the data from the control to the COleDataSource object. The final thing you do is put the COleDataSource object on the Clipboard, but only if you weren't the owner of the Clipboard at the time the transfer occurred. Setting the Clipboard again with the same object will result in an error.
Listing 7.17 MFCCONTROLWINCTL.CPP--CopyDataToClipboard Implementation void CMFCControlWinCtrl::CopyDataToClipboard(void) { // get the clipboard if we are the owner COleDataSource * opOleDataSource = COleDataSource::GetClipboardOwner(); BOOL bSetClipboard = FALSE; // if we didn't get back a pointer if(opOleDataSource == NULL) { // if this is a new clipboard object bSetClipboard = TRUE;
// get a new data source object opOleDataSource = new COleDataSource; } // call the common data preparation function this->PrepareDataForTransfer(opOleDataSource); // did we get a new clipboard object? if(bSetClipboard) // pass the data to the clipboard opOleDataSource->SetClipboard(); } PrepareDataForTransfer is used to create the necessary memory structures and to prepare the COleDataSource object that will be used in the data transfer (see Listing 7.18). For your data transfer, you are going to place the Caption on the Clipboard using the Clipboard format CF_TEXT. In order to put the text data on the Clipboard, you have to create a global memory object and copy the data to it. After you create the global object, you store it in the COleDataSource object using the CacheGlobalData function. Don't worry about cleaning up any data that was previously set in the COleDataSource object; the CacheGlobalData function will do that for you.
Listing 7.18 MFCCONTROLWINCTL.CPP-- PrepareDataForTransfer Implementation void CMFCControlWinCtrl::PrepareDataForTransfer(COleDataSource * opOleDataSource) { // get the length of the data to copy long lLength = m_cstrCaption.GetLength() + 1; // create a global memory object HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sizeof(TCHAR) * lLength); // lock the memory down LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(hGlobal); // copy the string for(long lCount = 0; lCount < lLength - 1; lCount++) lpTempBuffer[lCount] = m_cstrCaption.GetAt(lCount); // null terminate the string lpTempBuffer[lCount] = `\0'; // unlock the memory ::GlobalUnlock(hGlobal); // cache the data opOleDataSource->CacheGlobalData(CF_TEXT, hGlobal); } Enabling a Control as a Clipboard Target The opposite of being a Clipboard source is being a Clipboard target. First you need to update the CMFCControlWinCtrl class to include two new helper functions (see Listing 7.19).
Listing 7.19 MFCCONTROLWINCTL.H--Clipboard Target Support Helper Function Prototypes
. . . void GetDataFromClipboard(void); . . . BOOL GetDataFromTransfer(COleDataObject * opOleDataObject); }; . . . Getting data from the Clipboard is almost as simple as it is to put the data on the Clipboard in the first place. GetDataFromClipboard uses the COleDataObject MFC class to attach to the Clipboard and, if successful, passes the object to the GetDataFromTransfer helper function (see Listing 7.20).
Listing 7.20 MFCCONTROLWINCTL.CPP--GetDataFromClipboard Implementation void CMFCControlWinCtrl::GetDataFromClipboard(void) { // get a data object COleDataObject oOleDataObject; // attach it to the clipboard if(!oOleDataObject.AttachClipboard()) return; // transfer the data to the control this->GetDataFromTransfer(&oOleDataObject); } GetDataFromTansfer enumerates all of the available formats in the COleDataObject object using BeginEnumFormats (see Listing 7.21). To optimize the search a little bit, you should first see whether the Clipboard even contains any data that you can use. In this case, you are looking for the CF_TEXT format. The implementation contains a simple while loop that will execute as many times as there are formats in the COleDataObject object. When you locate a CF_TEXT format, you retrieve its global memory object with the function GetGlobalData and copy the data that it points to into the control. Do not destroy the data that was retrieved with this method--you are not its owner; the COleDataObject is.
Listing 7.21 MFCCONTROLWINCTL.CPP--GetDataFromTransfer Implementation BOOL CMFCControlWinCtrl::GetDataFromTransfer(COleDataObject * opOleDataObject) { BOOL bReturn = FALSE; // prepare for an enumeration of the clipboard formats available opOleDataObject->BeginEnumFormats(); // is there a text format available if(opOleDataObject->IsDataAvailable(CF_TEXT)) { FORMATETC etc; // while there are formats to enumerate while(opOleDataObject->GetNextFormat(&etc)) { // is this a format that we are looking for?
if(etc.cfFormat == CF_TEXT) { // get the global data for this format HGLOBAL hGlobal = opOleDataObject->GetGlobalData (etc.cfFormat, &etc); // lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock (hGlobal); // store the data m_cstrCaption = lpTempBuffer; // call the global routine this->FireChange(); // unlock the memory ::GlobalUnlock(hGlobal); // return success bReturn = TRUE; } } } // if we found a format if(bReturn == TRUE) // force the control to repaint itself this->InvalidateControl(NULL); // return the result return bReturn; } Custom Clipboard Formats The Clipboard is able to support custom formats, as well as built-in formats, defined by the operating system. Fortunately, the implementation of custom formats for the Clipboard is the same as for Drag and Drop. First you will learn how to support Drag and Drop and then how to support custom formats.
Drag and Drop Support The fundamentals of Drag and Drop support are very similar to Clipboard support and rely on the same MFC classes, COleDataSource and COleDataObject, for their implementation. Since you have broken your code into separate functions--that is, the Clipboard specific code is isolated from the basic data transfer functions--most of your implementation is complete. Using Built-In Drag and Drop Formats Since Drag and Drop is essentially a Clipboard transfer, with fewer steps involved, Drag and Drop uses the same built-in data formats as the Clipboard transfers do. See the list of supported formats earlier in this chapter. As with Clipboard transfers, there are two sides to the Drag and Drop coin. An application can be a Drag and Drop source or a Drag and Drop target, or both. Enabling a Control as a Drag and Drop Source To qualify as a Drag and Drop source, your only implementation requirements are that you create a COleDataSource object, call the method DoDragDrop, and have some way of initiating the Drag and Drop operation in the first place. In addition to the IDataObject and IEnumFORMATETC interfaces, the COleDataSource defines the IDropSource interface, all of which are necessary for Drag and Drop source support. The first step is to initiate a Drag and Drop operation. You are going to use the left mouse button down event, provided by MFC, to initiate the Drag and Drop operation. Open the ClassWizard, and select the Message Maps tab. Select the class CMFCControlWinCtrl, and double-click the WM_LBUTTONDOWN message to add the OnLButtonDown function (see fig. 7.3). To open the source file and add your code, double-click the OnLButtonDown function in the Member functions list box.
FIG. 7.3 Add the WM_LBUTTONDOWN message map. The OnLButtonDown implementation is similar to the Clipboard method CopyDataToClipboard (see Listing 7.22). Again, you are using a COleDataSource object and loading it with your data with the PrepareDataForTransfer call. To actually initiate the Drag and Drop operation, you call DoDragDrop specifying the constant DROPEFFECT_COPY, which indicates that this is a copy operation. The constant DROPEFFECT_COPY is used for two purposes. The first, and most obvious to the user, is that the mouse cursor will change to indicate that a copy drag is in progress. The second is to inform the drop target as to the intent of the drop operation.
Listing 7.22 MFCCONTROLWINCTL.CPP--OnLButtonDown Implementation void CMFCControlWinCtrl::OnLButtonDown(UINT nFlags, CPoint point) { COleDataSource oOleDataSource; // call the common data preparation function this->PrepareDataForTransfer(&oOleDataSource); // start the Drag and Drop operation oOleDataSource.DoDragDrop(DROPEFFECT_COPY); // call the base class implementation COleControl::OnLButtonDown(nFlags, point); } Now that the control has been enabled as a Drag and Drop source, it only makes sense to enable it as a Drag and Drop target. Enabling a Control as a Drop Target To qualify as a Drag and Drop target, a control must register itself as a Drag and Drop target and must implement the IDropTarget interface. The MFC class COleDropTarget provides the needed interface definition. However, it is not enough to rely on the default implementation of the COleDropTarget class to enable the control as a Drag and Drop target. You are required to inherit your own specialized version of the class so that you can override some of its functions. Call the new class CMyOleDropTarget, and add it to the CMFCControlWinCtl class (see Listing 7.23). You need to overload the methods OnDragOver and OnDrop for your specific implemen- tation as a drop target. Also added is a member variable of type CMyOleDropTarget called oMyOleDropTarget. The class CMyOleDropTarget needs to be a friend of the CMFCControlWinCtrl class so that the CMyOleDropTarget class may call the general GetDataFromTransfer function to store the data in the control.
Listing 7.23 MFCCONTROLWINCTL.H--CMyOleDropTarget Class Declaration Added to the CMFCControlWinCtl Class class CMyOleDropTarget: public COleDropTarget { public: CMFCControlWinCtrl * opMFCControlWinCtrl; DROPEFFECT OnDragOver(CWnd* pWnd, COleDataObject* pDataObject, DWORD
dwKeyState, CPoint point); BOOL OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point); } oMyOleDropTarget; friend CMyOleDropTarget; As we stated earlier, an application must register itself with the operating system in order to be a valid Drag and Drop target. Register the control as a Drag and Drop target using the function, Register, in your OnCreate method for the control. To add the OnCreate function to the class, you use the ClassWizard. Open the ClassWizard, and select the Message Maps tab. Select the class CMFCControlWinCtrl, and double-click the WM_CREATE message to add the OnCreate function to your class (see fig. 7.4). FIG. 7.4 Add the WM_CREATE message map. To add your code, open the source file by double-clicking the OnCreate function in the Member functions list box. Register is the only call that is required to enable your control as a drop target (see Listing 7.24). The member variable opMFCControlWinCtrl is set with a reference to the control so you can call the general method GetDataFromTransfer to put the data into your control; this is specific to your implementation and not a requirement for Drag and Drop target support.
Listing 7.24 MFCCONTROLWINCTL.CPP--OnCreate Implementation int CMFCControlWinCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (COleControl::OnCreate(lpCreateStruct) == -1) return -1; // let's register ourselves as drop targets oMyOleDropTarget.Register(this); // store a reference to the control so we can communicate back to the control oMyOleDropTarget.opMFCControlWinCtrl = this; return 0; } The OnDragOver function is used to instruct Windows that your control is a valid or invalid drop target for the current drag operation (see Listing 7.25). The implementation of OnDragOver first looks to see whether the mouse button is being held down. If it is, OnDragOver then looks for the availability of the CF_TEXT format. If it finds the CF_TEXT format, the OnDragOver function returns the constant DROPEFFECT_COPY, indicating to Windows that the control is a valid drop target for the particular drop operation that is currently in effect. Otherwise, OnDragOver returns DROPEFFECT_NONE, indicating that a drop should not be allowed.
Listing 7.25 MFCCONTROLWINCTL.CPP--OnDragOver Implementation DROPEFFECT CMFCControlWinCtrl::CMyOleDropTarget::OnDragOver(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) { // if the left mouse button is being held down
if(dwKeyState | MK_LBUTTON) { // is there a text format available if(pDataObject->IsDataAvailable(CF_TEXT)) return DROPEFFECT_COPY; // everything else we can't deal with else return DROPEFFECT_NONE; } else // not the left mouse return DROPEFFECT_NONE; }
OnDrop is where you call GetDataFromTransfer, the same function that you created for your Clipboard operations, to copy the data from the drop source into your control (see Listing 7.26).
Listing 7.26 MFCCONTROLWINCTL.CPP--OnDrop Implementation BOOL CMFCControlWinCtrl::CMyOleDropTarget::OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point) { // transfer the data to the control return opMFCControlWinCtrl->GetDataFromTransfer(pDataObject); }
Custom Drag and Drop Formats Like the Clipboard, Drag and Drop can also support custom data transfer formats. The next section will examine in detail how to modify your existing code to support custom data formats for Clipboard and Drag and Drop operations, both with only a single code change to the source and target data transfer operations.
Custom Clipboard and Drag and Drop Formats Custom Clipboard and Drag and Drop formats are a way for applications to trade information on a more intimate basis. The data transferred can be of any type and structure that the applications can create and use. When using custom Clipboard formats, all applications that will use the format must first call RegisterClipboardFormat passing in the name of the format. Before you add the code to register your custom format, add a member variable, m_uiCustomFormat, to your class definition (see Listing 7.27). You will use the value that this member variable holds later in your implementation to determine whether a valid format has been registered.
Listing 7.27 MFCCONTROLWINCTL.H--m_uiCustomFormat Member Variable Added to the CMFCControlWinCtrl Class . . . // custom format storage variable UINT m_uiCustomFormat;
};
Now add the custom format registration code to the constructor of the class CMFCControlWinCtrl (see Listing 7.28). The RegisterClipboardFormat function, if it succeeds, will return the ID of the newly registered or already existing (in the case where the format is already registered) custom Clipboard format. It is important that all applications that are going to use the custom format call this method.
Listing 7.28 MFCCONTROLWINCTL.CPP--Custom Clipboard Format Registered in the CMFCControlWinCtrl Constructor CMFCControlWinCtrl::CMFCControlWinCtrl() { InitializeIIDs(&IID_DMFCControlWin, &IID_DMFCControlWinEvents); // TODO: Call InternalSetReadyState when the readystate changes. m_lReadyState = READYSTATE_LOADING; // set the alignment to the default of left m_lAlignment = EALIGN_LEFT; // don't forget this - DoPropExchange won't work without it oMyDataPath.SetControl(this); // register a custom clipboard format m_uiCustomFormat = ::RegisterClipboardFormat(_T("MFCControlWinCustomFormat")); } After the custom format has been registered, it is a simple matter to add the code to your implementation that stores or retrieves the data from the COleDataSource and COleDataObject objects. Using the code that was created for your standard data transfers as a base, you will now add the custom data transfer code. PrepareDataForTransfer (see Listing 7.29) differs only slightly from its original implementation. For the custom data transfer, you are going to send the m_lAlignment value in addition to the caption. You can use any data type or structure, including user-defined data types, for your custom format. The only requirement is that both the source and the target applications understand the format being transferred.
Listing 7.29 MFCCONTROLWINCTL.CPP--PrepareDataForTransfer Function Updated to Support Custom Clipboard Formats void CMFCControlWinCtrl::PrepareDataForTransfer(COleDataSource * opOleDataSource) { // get the length of the data to copy long lLength = m_cstrCaption.GetLength() + 1; // create a global memory object HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sizeof(TCHAR) * lLength); // lock the memory down LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(hGlobal);
// copy the string for(long lCount = 0; lCount < lLength - 1; lCount++) lpTempBuffer[lCount] = m_cstrCaption.GetAt(lCount); // null terminate the string lpTempBuffer[lCount] = `\0'; // unlock the memory ::GlobalUnlock(hGlobal); // cache the data opOleDataSource->CacheGlobalData(CF_TEXT, hGlobal); // if we have custom clipboard format support if(m_uiCustomFormat) { // create a global memory object HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sizeof(m_lAlignment)); // lock the memory down LONG * lpTempBuffer = (LONG *) ::GlobalLock(hGlobal); // set our data buffer *lpTempBuffer = m_lAlignment; // unlock the memory ::GlobalUnlock(hGlobal); // cache the data opOleDataSource->CacheGlobalData(m_uiCustomFormat, hGlobal); } }
NOTE: Any number of data formats can be transferred at one time for a single data source object. It is up to the receiving application to deal with this possibility and retrieve the data correctly. The MFC implementation of the COleDataSource class does not allow duplicate formats to be available in the same object, but that's not to say that this can't happen. Non-MFC applications have complete freedom to implement data sources any way they see fit. Remember yours may not be the only data being transferred.
The GetDataFromTransfer implementation is almost identical to your original implementation (see Listing 7.30). The only difference is that the function now looks for the custom format as well as the CF_TEXT format.
Listing 7.30 MFCCONTROLWINCTL.CPP--GetDataFromTransfer Function Updated to Support Custom Clipboard Formats BOOL CMFCControlWinCtrl::GetDataFromTransfer(COleDataObject * opOleDataObject) { BOOL bReturn = FALSE; // prepare for an enumeration of the clipboard formats available opOleDataObject->BeginEnumFormats(); // is there a text format available // && there is no custom format
// || there is a custom format and the format is available if(opOleDataObject->IsDataAvailable(CF_TEXT) && (!m_uiCustomFormat || (m_uiCustomFormat && opOleDataObject->IsDataAvailable (m_uiCustomFormat)))) { FORMATETC etc; // while there are formats to enumerate while(opOleDataObject->GetNextFormat(&etc)) { // is this a format that we are looking for? if(etc.cfFormat == CF_TEXT) { // get the global data for this format HGLOBAL hGlobal = opOleDataObject->GetGlobalData (etc.cfFormat, &etc); // lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(hGlobal); // store the data m_cstrCaption = lpTempBuffer; // call the global routine this->FireChange(); // unlock the memory ::GlobalUnlock(hGlobal); // return success bReturn = TRUE; } // if we have custom clipboard format support else if(m_uiCustomFormat && etc.cfFormat == m_uiCustomFormat) { // get the global data for this format HGLOBAL hGlobal = opOleDataObject->GetGlobalData (etc.cfFormat, &etc); // lock the memory down LONG * lpTempBuffer = (LONG *) ::GlobalLock(hGlobal); // get the data from our data buffer m_lAlignment = *lpTempBuffer; // unlock the memory ::GlobalUnlock(hGlobal); // return success bReturn = TRUE; } } } // if we found a format if(bReturn == TRUE) // force the control to repaint itself this->InvalidateControl(NULL); // return the result return bReturn; } Because both the Clipboard support and the Drag and Drop support rely on the same data transfer routines to exchange data, you have the added benefit of supporting the custom data formats for each, while having to maintain only two functions, instead of four.
Subclassing Existing Windows Controls Since the early days of Windows programming, programmers have enjoyed the option of using the default behavior of existing Windows controls and extending them slightly to create new and more powerful controls. This technique of creating Windows controls is referred to as subclassing (and there is also superclassing, depending on the technique you use). The same is still true for ActiveX controls. When you created the original application code with the MFC AppWizard, you created a total of three controls. One of those controls, CMFCControlSubWin, subclassed a Windows BUTTON control. Listing 7.31 contains all of the additional code that is required of an ActiveX control to subclass a control, which, by the way, was generated for you automatically by the MFC AppWizard when you created the project.
Listing 7.31 MFCCONTROLSUBWINCTL.CPP--Additional Code Requirements for ActiveX Controls that Subclass Existing Windows Controls ///////////////////////////////////////////////////////////////////////////// // CMFCControlSubWinCtrl::OnDraw - Drawing function void CMFCControlSubWinCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { DoSuperclassPaint(pdc, rcBounds); } . . . ///////////////////////////////////////////////////////////////////////////// // CMFCControlSubWinCtrl::PreCreateWindow - Modify parameters for CreateWindowEx BOOL CMFCControlSubWinCtrl::PreCreateWindow(CREATESTRUCT& cs) { cs.lpszClass = _T("BUTTON"); return COleControl::PreCreateWindow(cs); } ///////////////////////////////////////////////////////////////////////////// // CMFCControlSubWinCtrl::IsSubclassedControl - This is a subclassed control BOOL CMFCControlSubWinCtrl::IsSubclassedControl() { return TRUE; } ///////////////////////////////////////////////////////////////////////////// // CMFCControlSubWinCtrl::OnOcmCommand - Handle command messages LRESULT CMFCControlSubWinCtrl::OnOcmCommand(WPARAM wParam, LPARAM lParam) { #ifdef _WIN32 WORD wNotifyCode = HIWORD(wParam); #else WORD wNotifyCode = HIWORD(lParam); #endif
// TODO: Switch on wNotifyCode here. return 0; } OnDraw is where you call DoSuperClassPaint to instruct the subclassed control to paint itself. Calling DoSuperClassPaint will work only for those controls that accept a device context handle (HDC) as the WPARAM of their WM_PAINT message. PreCreateWindow is where the most important action of the whole subclassing process occurs; you identify, by name, the control to subclass. The name used can be any valid Windows control, including user-defined controls. As long as the control is a registered, valid Windows control, you can subclass it. It's pretty obvious what IsSubclassedControl is used for. MFC queries the function to determine whether it subclasses a Windows control. OnOcmCommand is where you get all of your reflected windows messages. Within this function, you can respond to messages like button clicks and edit change notifications. Microsoft has created sample code and plenty of documentation regarding control subclassing, so we won't get into any implementation details here.
NOTE: Note that a subclassed control may not behave exactly as its original implementation does. In the case of subclassing a list box, the font that the control uses may not match that of the container. Getting the subclassed list box to use the correct font can be a little difficult and may require sending messages directly to the subclassed window rather than allowing the MFC base implementation to define the font. Subclassing an existing control is a risky proposition at best. The implementation of the particular Windows control you are subclassing may make it difficult, if not impossible, to subclass from within the confines of the MFC control framework. The concept of multiple windows' handles within a single control is one area where subclassing using MFC will come up short. It may be very difficult to reliably retrieve the secondary window handles of the control if the original implementation does not provide access to its child windows. Also, some of the ActiveX features detailed in this chapter may not be appropriate or possible because you are simply wrapping an existing window's control. Creating a windowless control is impossible because you are depending on the window's control for the implementation.
Dual-Interface Controls At this time, none of the control containers can, or will, take advantage of dual-interfaces implemented in a control, but that is not to say that they won't in the future. The addition of dual-interface support to your control is exactly the same as adding dual-interface to ActiveX Automation Servers. It will add only a little more code to your application and should result in significant performance improvement for the control if and when containers are built to support dual-interface. Microsoft strongly suggests that controls be created with dual-interface support, in spite of the fact that, at the time
of the writing of this book, none of Microsoft's containers can use them.
Other ActiveX Features Finally, the section that you all have been waiting for: Advanced ActiveX features. Don't stand up and jump just yet. Most of the features require little code, if any at all, and really aren't intended for anything more than trimming a few more precious milliseconds from your load times and runtimes. When the AppWizard creates the basic source files for your control, it adds a member function GetControlFlags to your class header and source files. GetControlFlags is where the control informs MFC about the kind of ActiveX support it has by returning a set of flags. The documentation for GetControlFlags lists all of the valid flags that can be returned from this function, thereby indicating the control's level of ActiveX support. You covered the features, optimized drawing and asynchronous properties, earlier in the chapter. Now take a look at the implementation specifics for the rest of the features.
Windowless Activation Setting this flag will allow a control to be created without the burden of creating a window, significantly improving the control's start-up time. The implementation of windowless controls versus windowed controls, for the most part, will remain unchanged since most of the windowless details are hidden from the control programmer. For your own implementation of the CMFCControlWinCtrl class, nothing that you did in your implementation would prevent you from using this feature. The main restriction when writing a windowless control is that you cannot access the window handle directly without first checking to see whether it is valid. If your control implementation never uses the window handle, you should be fine. To prove the point regarding the implementation of windowed and windowless controls, the CMFCControlNoWin class's implementation of OnDraw contains the same code as your windowed implementation. One thing to note though is that the container is the deciding factor when it comes to windowed versus windowless support. A window will be created for the control automatically if the container does not have windowless support. This is a requirement for all controls, regardless of the tool used to create them. See the VC++ books online article "OLE Controls: Optimization" for the specific implementation details regarding message maps and routing for windowless controls.
Flicker-Free Activation Flicker-free activation simply says that the control will not receive a notification to draw its UI when it transitions between an active and inactive state, and vice versa. The only requirement of a control with this feature is that the control's UI remains the same regardless of the state of the control.
Unclipped Device Context
Unclipped device context is a commitment more than a feature. When this flag is set, the control is telling the container that it will not draw outside of its client area. Setting the unclipped device context flag will improve performance by eliminating unnecessary clipping tests. Unclipped device context cannot be used with the windowless controls.
Mouse Pointer Notifications When Inactive Mouse pointer notifications when inactive allows the control to receive mouse notification messages, even though the window may not be active. Mouse pointer notifications when inactive is used for those users who turned off the feature "Activate when visible" when first creating their control project with the AppWizard. The only special circumstance to be concerned with is the case of Drag and Drop operations, which require an activated window. The programmer of the control must override the control's GetActivationPolicy function to instruct the container how to activate the control when it becomes the target of a Drag and Drop operation. See the VC++ books online article "OLE Controls: Optimization" for the specific implementation details regarding this feature.
From Here... In this chapter, we demonstrated fairly simple techniques for creating unique and interesting controls and control features. We built upon simple concepts and methods that, in turn, resulted in a control that is far greater than the sum of its parts. A little bit of work and forethought can go a long way when using MFC. MFC provides a large and robust framework for creating ActiveX controls. The ease of use and support provided by the AppWizard, ClassWizard, and the VC++ IDE make MFC unbeatable for rapid control development. Unfortunately, those very same features and functionality make MFC a hard choice when building small, fast controls for use over the Internet or in applications where performance is an issue. DLL load times and unnecessary code overhead can make MFC unreasonable to use for simple, lightweight controls. On the other hand, MFC hides a large number of the details surrounding control implementation and leaves you free to focus on the control and its specific implementation details. All of these things must be considered when choosing MFC as a control development framework. The next four chapters examine in detail how to create a similar control implementation using ATL and BaseCtl.
Chapter 8 Using ATL to Create a Basic ActiveX Control ●
Using ATL to Create a Basic ActiveX Control ❍ Creating the Basic Control Project ❍ Control Registration ■ Listing 8.1 ATLCONTROLWIN.RGS--Sample Registry Script File for the CATLControlWin Control Class ❍ Creating Methods ■ Listing 8.2 ATLCONTROLWIN.H--Alignment Enumeration Include File and Member Variables Added to Class Definition ■ Listing 8.3 ALIGNMENTENUMS.H--Alignment Enumeration Include File ■ Listing 8.4 ATLCONTROLWIN.H--Member Variable Initialization ■ Listing 8.5 ATLCONTROLWIN.CPP--CaptionMethod Implementation ❍ Properties ■ Creating Normal User Defined Properties ■ Listing 8.6 ATLCONTROL.IDL--Dispid Enumeration Added to the IDL File to Aid in the Support of Properties in the Control ■ Listing 8.7 ATLCONTROLWIN.CPP--Alignment Property Get/Put Method Implementation ■ Creating Parameterized User Defined Properties ■ Listing 8.8 ATLCONTROL.IDL--Update the IDL File to Support the Parameterized Property ■ Listing 8.9 ATLCONTROLWIN.CPP--get_CaptionProp Implementation ■ Listing 8.10 ATLCONTROLWIN.CPP--SetCaptionProp Implementation ■ Creating Stock Properties ■ Listing 8.11 ATLCONTROL.IDL--Add the Constant DISPID_BACKCOLOR to the IDL to Support the BackColor Stock Property ■ Listing 8.12 ATLCONTROLWIN.H--m_BackColor Member Variable Added to the Class Declaration ■ Listing 8.13 ATLCONTROLWIN.CPP--BackColor Property Source File Implementation ■ Listing 8.14 ATLCONTROLWIN.H--Initialize the BackColor Property to an Initial Value ■ Using Ambient Properties ■ Creating Property Sheets ■ Listing 8.15 ATLCONTROLWINPPG.H--Add the Necessary Include Files to the Property Page Header File ■ Listing 8.16 ATLCONTROLWINPPG.H--Add the OnInitDialog Message Handler so the Property Page Can Be Initialized ■ Listing 8.17 ATLCONTROLWINPPG.H--Modify the Apply Function to Update All of the Properties in the Control When the Property Page Exits ■ Listing 8.18 ATLCONTROLWIN.H--Add the Property Page Reference to the Control Class Declaration to Complete the Property Page Implementation ❍ Adding Events ■ Listing 8.19 ATLCONTROL.IDL--Add the Event Interface and the Change Method to the ATLControl.IDL File ■ Listing 8.20 ATLCONTROLWIN.CPP--FireChange Helper Function Added to the Control ■ Listing 8.21 ATLCONTROLWIN.CPP--FireChange Event Added to the CaptionMethod
❍
❍
❍
Implementation Persistence ■ Listing 8.22 ATLCONTROLWIN.H--BackColor Property Added to the Property Map for Persistence Drawing the Control ■ Standard Drawing ■ Listing 8.23 ATLCONTROLWIN.H--Drawing Implementation Member Variables and Functions ■ Listing 8.24 ATLCONTROLWIN.H--Initialize the New Member Variables in the Constructor ■ Listing 8.25 ATLCONTROLWIN.CPP--Drawing Helper Functions ■ Listing 8.26 ATLCONTROLWIN.CPP--Standard Drawing Added to the OnDraw Function From Here...
Using ATL to Create a Basic ActiveX Control ●
●
●
●
●
●
Registration ATL's support of registration scripts makes registration support even easier to implement and use. Adding methods The ATL Object Wizards, while a little rudimentary, provide the necessary IDE integration that makes ATL appealing for rapid development. Adding properties The ATL Object Wizard makes adding properties a snap. Adding events Unfortunately, event support is not as easy to implement as that of MFC, but this chapter sheds some light on the subject that should make it as easy as is possible. Persistence ATL persistence support makes up for events in terms of ease of implementation. Drawing the control ATL deviates very little from the norm when it comes to drawing the control and requires no special knowledge to implement.
With the coming of Visual C++ 5.0, the ActiveX Template Library (ATL) has matured to the level necessary for a complete ActiveX development framework. The two releases of ATL, versions 1.0 and 1.1, that came before VC++ 5.0 introduced ATL to the growing ActiveX development community as an alternative to MFC. However, the first two versions of ATL allowed only for the creation of ActiveX COM Objects and ActiveX Automation Servers (no small feats in their own right). ATL version 2.1, the version that ships with VC++ 5.0, now supports the creation of ActiveX Controls. Versions 1.0 and 1.1 included an AppWizard for creating a basic ATL project. Version 2.1 includes a number of AppWizards that can be used to create various ActiveX components, thus furthering ATL's capability to compete
with MFC as a rapid development tool. In this chapter, you will create an ActiveX control with all the basics: methods, properties, events, persistence, and drawing. Also, in this chapter and Chapter 9, you will explore some of the more advanced features and lesser known aspects of control development, such as methods with optional parameters, asynchronous properties, Clipboard support, and optimized drawing, to name a few.
Creating the Basic Control Project To create an ATL ActiveX control, you want to take advantage of the AppWizard provided by Visual C++. Run the Visual C++ development environment, and from the File menu, select New. When the New dialog displays, select the Projects tab (see fig 8.1). The Projects tab allows you the opportunity to define several aspects of how the application will be created, for example, the type of application to create, the name of the application, and the location where you want the project created. For the type, select ATL COM AppWizard; enter the Project Name ATLControl, and the Location will be C:\que\ActiveX\ATLControl. Click the OK button to start the ATL COM AppWizard so you can further define the properties of your control. FIG. 8.1 Define the new ATL control project with the New dialog. The next step in the AppWizard is defining the basic architecture of your ATL project (see fig. 8.2). Since you are creating an ActiveX Control, you choose the Dynamic Link Library (DLL) radio button. FIG. 8.2 Define the basic architecture of the ATL COM Object with the ATL COM AppWizard.An OCX is in reality nothing more than a DLL. The extension OCX was carried over from the early days of control development. You have the option of changing the extension from DLL to OCX. Since the sample implementation will be a control, it is not necessary to support merging of the proxy/stub marshaling code, nor will the implementation require the use of MFC. Click the Finish button to continue. The New Project Information dialog is used to confirm the settings that were selected for the project prior to the creation of the actual source files (see fig. 8.3). This step is the last one in the ATL COM AppWizard. FIG. 8.3 Confirm the new project settings with the New Project Information dialog. "But wait," you say, "I haven't defined any of my control properties." The ATL COM AppWizard takes a slightly different approach from that of MFC. Only the basic source files are created with the AppWizard. The remainder of the project is defined by the Object Wizard, which allows much better control of the project implementation versus MFC since the developer can add any number of ActiveX Controls, Servers, or plain COM Objects after the basic project is created. After you confirm your project settings, click the OK button to close the ATL COM AppWizard and create the ATLControl project. The next step is to add your control implementations to the project. From the Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard, select the Controls item in the left panel to display the types of control components that can be added (see fig. 8.4). Your implementation will be a Full Control, so select the Full Control icon. The other types of components that can be created are a Microsoft Internet Explorer control that supports all of the necessary interfaces to be hosted by the Internet Explorer Web browser and a Property Page component, which you need if your control requires property page support. The Internet Explorer control simply
supports fewer interfaces than the Full Control. The Full Control will also work within a Web browser. For more information, see the ATL documentation. Click the Next button to continue. FIG. 8.4 Select the type of ATL object to add to your project. The next dialog is the ATL Object Wizard Properties dialog, which is used to define the specific properties of the new object that will be added to your project. Select the Names tab, and in the Short Name edit field, type ATLControlWin (see fig. 8.5). The remainder of the edit fields will automatically update, reflecting the short name that you added. The other fields can be changed, but in this case, you use the default values. FIG. 8.5 Define the name of the new control object. Select the Attributes tab so that you can define the attributes of the control project (see fig. 8.6). Check the Support ISupportErrorInfo and Support Connection Points check boxes to add OLE rich error support and events to the control. See Chapter 4 and the ATL documentation for more information regarding the options available to you. Leave the remainder of the settings at their default values. FIG. 8.6 Define the attributes of the new control object. You use the Miscellaneous tab to define how the control will draw and act while contained and whether your control implementation subclasses a built-in Windows control (see fig. 8.7). For your implementation, you want the control to always create a window whether or not the container is capable of supporting windowless controls, so check the Windowed Only check box. Leave the remainder of the controls at their default settings. FIG. 8.7 The Miscellaneous tab is used to define some of the basic control behaviors. The Stock Properties tab is used to define any number of the basic stock properties that the control project will support (see fig. 8.8). For now, leave the Stock Properties tab as is, and click the OK button to create the new control object. FIG. 8.8 The Stock Properties tab is used to define the stock properties that the control object will be created with. As with the MFC and later the BaseCtl implementation, you need to define two more controls to complete the sample implementation. From the Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard, select the Controls item in the left panel, and select the Full Control icon (refer to fig. 8.4). Click the Next button to continue. On the Names tab within the ATL Object Wizard Properties dialog, add the Short Name ATLControlNoWin, and on the Attributes tab, check the Support ISupportErrorInfo and Support Connection Points check boxes to add OLE rich error support and events to the control. On the Miscellaneous tab, do not check the Windowed Only check box--so that the control will create a window for itself only if the container cannot. Leave the Stock Properties tab at its default settings. Click OK to add the control object to the project. For the last control implementation, you create a control that subclasses another window's control. Again, from the
Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard (refer again to fig. 8.4), select the Controls item in the left panel, and select the Full Control icon. Click the Next button to continue. On the Names tab, within the ATL Object Wizard Properties dialog, add the Short Name ATLControlSubWin, and on the Attributes tab, check the Support ISupportErrorInfo and Support Connection Points check boxes to add OLE rich error support and events to the control. On the Miscellaneous tab, select the Button control from the Add control based on list box, and check the Windowed Only check box to ensure that a window is always created for the control whether the container supports windowless controls or not. Leave the Stock Properties tabs at its default settings. Click OK to add the control object to the project. At this point in the MFC sample (see Chapter 6), you are also able to add other ActiveX features as part of the AppWizard implementation. The ATL AppWizard and Object Wizard do not allow for defining any other ActiveX features at this point, but they are defined in Chapter 9. All of the basic source files and control objects are now added to the control project. The next step in any control project is to ensure that the project contains registration support. Without registration, the control cannot be used by any application.
Control Registration Control registration and unregistration support is provided for you by ATL. You are not required to make any code changes or additions to support it. Unlike MFC, which uses a set of constants, ATL relies on resource information in the form of a registry script file to define the information that is added to the registry database. The registry script file is added automatically to the project when the control object is added; one script file is added for each control object. The registry script file or files are compiled into the control project as resources and can be viewed in binary form in the resource editor. The files, which have the extension .rgs, are normal text files that can be edited within the IDE. For more information about the use of registry script files and their particular syntax, see the VC++ books online subject "Registry Scripting Examples--Active Template Library, Articles." Listing 8.1 shows the registry script file for the CATLControlWin control object that you added.
Listing 8.1 ATLCONTROLWIN.RGS--Sample Registry Script File for the CATLControlWin Control Class
HKCR { ATLControlWin.ATLControlWin.1 = s `ATLControlWin Class' { CLSID = s `{A19F6964-7884-11D0-BEF3-00400538977D}' } ATLControlWin.ATLControlWin = s `ATLControlWin Class' { CurVer = s `ATLControlWin.ATLControlWin.1' } NoRemove CLSID
{ ForceRemove {A19F6964-7884-11D0-BEF3-00400538977D} = s `ATLControlWin Class' { ProgID = s `ATLControlWin.ATLControlWin.1' VersionIndependentProgID = s `ATLControlWin.ATLControlWin' ForceRemove `Programmable' InprocServer32 = s `%MODULE%' { val ThreadingModel = s `Apartment' } ForceRemove `Control' ForceRemove `Programmable' ForceRemove `Insertable' ForceRemove `ToolboxBitmap32' = s `%MODULE%, 1' `MiscStatus' = s `0' { `1' = s `131473' } `TypeLib' = s `{A19F6957-7884-11D0-BEF3-00400538977D}' `Version' = s `1.0' } } } You can now compile and register the control you've created, but it won't be of much use because it doesn't contain any methods, properties, or events.
Creating Methods Creating Methods Now that you have successfully created your basic ActiveX control project, you can add a method, which is one of the basic aspects of component development. For the purposes of the sample control, you are going to add a method called CaptionMethod. The method will accept two parameters, the second being optional. The first parameter is a string that the control will display within its client area, and the second, optional parameter is the alignment of the caption within the client area, either left, right, or center. Adding methods to an ATL control differs from MFC in that MFC relies on the familiar ClassWizard, and ATL does not. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface, click the right mouse button, and select the Add Method menu item (see fig. 8.9). In the Add Method to Interface dialog, add the Method Name, CaptionMethod, and the Parameters, [in] BSTR bstrCaption, [in, optional] VARIANT varAlignment, [out, retval] long * lRetVal (see fig. 8.10). The Attributes button displays a dialog for adding Interface Definition Language (IDL) attributes for the entire function declaration. FIG. 8.9 Add a new method to the control project. FIG. 8.10
Define the CaptionMethod method.
NOTE: All optional parameters must be of type VARIANT, and they must fall at the end of the parameter list. Optional parameters are not managed in any way by OLE. It is the Server application's responsibility to determine whether the VARIANT parameter passed to the method contains data and whether to ●
either use the data passed to the method or convert the data to a useful type, if possible, or
●
ignore the parameter if invalid data was passed and use the default value if appropri- ate, or
●
inform the user of an error condition if one of the above conditions was not met.
You've also added the direction that the parameters flow in the form of [in] and [out] parameter attributes. See Table 8.1 for a complete description of the possible attributes that can be used. Parameter attributes are used to aid development tools in determining how parameters are used within a function call. A tool like Visual Basic will hide the details of how parameters are handled--such as creating and destroying memory--based on these and other attributes in the type library. This is why the type library is so important to ActiveX component development. Note that the IDL parameter attributes are added directly to the parameter list. Click OK to add the method to the control. Table 8.1 Parameter Flow Attributes Direction
Description
in
Parameter is passed from caller to callee.
out
Parameter is returned from callee to caller.
in, out
Parameter is passed from caller to callee, and the callee returns a parameter.
out, retval Parameter is the return value of the method and is returned from the callee to the caller.
To aid your CaptionMethod implementation, you need to add an enumeration for all the valid alignment settings and two member variables to your class definition (see Listing 8.2). The enumeration is included in the header file Alignmentenums.h (see Listing 8.3). The two member variables, m_lptstrCaption and m_lAlignment, are used to store the caption string and the alignment setting while the control is being used. Note the data type used for the m_lAlignment member variable. The variable is declared as type long and not as the enumeration type because of the data type restrictions imposed upon you by ActiveX Automation. Remember that only data types that can be passed in a VARIANT can be used in methods and properties. By declaring the m_lAlignment member as long, you do not have to explicitly convert the value by casting to the enumerated type when it is retrieved from the VARIANT parameter in the caption method. On the other hand, casting the value to the enumerated type is a trivial issue, and its implementation is based completely on your preference.
Listing 8.2 ATLCONTROLWIN.H--Alignment Enumeration Include File and
Member Variables Added to Class Definition
// ATLControlWin.h : Declaration of the CATLControlWin #ifndef __ATLCONTROLWIN_H_ #define __ATLCONTROLWIN_H_ #include "resource.h" // main symbols #include "alignmentenums.h" ///////////////////////////////////////////////////////////////////////////// // CATLControlWin class ATL_NO_VTABLE CATLControlWin : . . . protected: // storage variable for the caption LPTSTR m_lptstrCaption; // storage variable for the alignment long m_lAlignment; }; The enumeration is added as an include file. By adding the enumeration to an include file, you are able to use the enumeration in other files simply by including the file reference, which will be necessary as you proceed through the chapter (see Listing 8.3).
Listing 8.3 ALIGNMENTENUMS.H--Alignment Enumeration Include File
#if !defined _ALIGNMENTENUMS_H #define _ALIGNMENTENUMS_H // caption alignment enumeration typedef enum tagAlignmentEnum { EALIGN_LEFT = 0, EALIGN_CENTER = 1, EALIGN_RIGHT = 2, }EALIGNMENT; #define EALIGN_LEFT_TEXT "Left" #define EALIGN_CENTER_TEXT "Center" #define EALIGN_RIGHT_TEXT "Right" #endif // #if !defined _ALIGNMENTENUMS_H You initialize your member variables in the constructor of your control (see Listing 8.4).
Listing 8.4 ATLCONTROLWIN.H--Member Variable Initialization
. . . public IConnectionPointContainerImpl, public ISpecifyPropertyPagesImpl
{ public: CATLControlWin() { // NULL terminate the string reference m_lptstrCaption = new TCHAR[1]; m_lptstrCaption[0] = `\0'; // set the alignment to the default of left m_lAlignment = EALIGN_LEFT; } DECLARE_REGISTRY_RESOURCEID(IDR_ATLCONTROLWIN) . . . The CaptionMethod contains all of the code for setting the caption and the alignment style, and like the MFC implementation, deals with the optional parameter correctly (see Listing 8.5). See Chapter 6 for more information about optional parameters and their use. Since the CaptionMethod is used for the IDispatch implementation and the custom interface, the method is implemented in a slightly different way than its MFC counterpart. First the function is declared as STDMETHODIMP, which expands to an HRESULT return type. The return value is used by OLE to determine whether the method call succeeded. The string parameter is passed in differently also. All strings are passed as UNICODE in OLE. This is true even for MFC. The only difference is that MFC hides the implementation details of how the strings are managed; the developer simply uses the appropriate string data type based on the target application and platform, that is, Win32 ANSI versus Win32 UNICODE. Note the use of the USES_CONVERSION and W2A macros to convert the string from UNICODE to ANSI.
Useful Helper Functions and Conversion Macros The files ATLCONV.H and ATLCONV.CPP, which can be found in the directory ...\DevStudio\VC\ATL\include, contain a number of helper functions and macros for converting data such as UNICODE strings to ANSI. Since ATL does not require the use of MFC, you are wise to examine these files before writing functions to convert data.
Next, if the VARIANT is of a valid data type other than VT_I4, the method tries to convert it to a VT_I4 type. You try to convert the data for the cases where a user passes valid data in the form of a different data type, for example, a short or a string. One very important thing to note is the use of the function VariantInit:. All VARIANT variables must be initialized prior to their use. This practice guarantees that the VARIANT does not contain invalid data type information or invalid values. This practice follows the basic C++ tenet of initializing all member variables to ensure that they do not contain invalid information. If the requirements of your control demand that you deal with only specific data types, you can also choose to add code (error messages, exceptions, and so on) to deal with the fact that the method did not receive a valid data type. If the function VariantChangeType is unable to convert the data, the method exits and returns a value of FALSE. A return of FALSE indicates to the caller of the method that the method didn't succeed. Again, you can also choose to add additional error handling code to the method to give the user more information about the error that occurred. See Chapters 3 through 5 on generating OLE exceptions for more information.
Before proceeding, the method ensures that the m_lAlignment member variable contains valid data. If the method received valid data or converted the data to a valid value, as indicated by the variable lResult equaling TRUE, the method stores the caption and the alignment values in the class member variables, invalidates the control so it will redraw its User Interface (UI) based on the new information, and exits the function. Listing 8.5 contains another important difference from that of its MFC counterpart: use of the function FireViewChange in place of the MFC InvalidateControl function to force the control to repaint itself. Wherever appropriate, we will point out the differences between MFC and ATL.
Listing 8.5 ATLCONTROLWIN.CPP--CaptionMethod Implementation
STDMETHODIMP CATLControlWin::CaptionMethod(BSTR bstrCaption, VARIANT varAlignment, long * lRetVal) { // needed for the W2A macro USES_CONVERSION; HRESULT hResult = S_OK; // return value initialized to failure result *lRetVal = FALSE; // convert the string to ANSI LPTSTR lptstrTempCaption = W2A(bstrCaption); // if the variant is a long just use the value if(VT_I4 == varAlignment.vt) { // assign the value to our member variable m_lAlignment = varAlignment.lVal; // set the return value *lRetVal = TRUE; } // if the user didn't supply an alignment parameter we will assign the default else if(VT_ERROR == varAlignment.vt || VT_EMPTY == varAlignment.vt) { // assign the value to our member variable m_lAlignment = EALIGN_LEFT; // set the return value *lRetVal = TRUE; } else { // get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful // VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType(&varConvertedValue, (VARIANT *) &varAlignment, 0, VT_I4))
{ // assign the value to our member variable switch(varConvertedValue.lVal) { case EALIGN_CENTER: m_lAlignment = EALIGN_CENTER; break; case EALIGN_RIGHT: m_lAlignment = EALIGN_RIGHT; break; default: m_lAlignment = EALIGN_LEFT; break; } // set the return value *lRetVal = TRUE; } else { // at this point we could either throw an error indicating // there was a problem converting // the data or change the return type of the method and // return the HRESULT value from the // the "VariantChangeType" call. } } // if everything was OK if(TRUE == *lRetVal) { // if we have a string if(lptstrTempCaption != NULL) { // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lptstrTempCaption) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lptstrTempCaption); } // did they pass us bad data? if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT) // sure did, lets fix their little red wagon m_lAlignment = EALIGN_LEFT; // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); = EALIGN_LEFT && newVal SetDirty(TRUE); // this->SetModifiedFlag(); FireOnChanged(dispidAlignment); // this->BoundPropertyChanged(dispidAlignment); FireViewChange(); // this->InvalidateControl(); get_Alignment(&lAlignment); // get the current selection in the listbox ::SendMessage(::GetDlgItem(m_hWnd, IDC_ALIGNMENTCOMBO), CB_SETCURSEL, lAlignment, 0); return TRUE; } LRESULT OnAlignmentComboSelChange(WORD wNotify, WORD wID, HWND hWnd, BOOL& bHandled) { SetDirty(TRUE); return FALSE; } . . . The property page contains a combo box that the user can use to change the value of the Alignment property. When the user changes the value, the property page has to be notified of the change so that the property page will update the control with the new property value. As for the WM_INITDIALOG message, you are required to add a WM_COMMAND message handler for the CBN_SELCHANGE message that the combo box will fire when its value changes. Adding the message handler is done through a slightly different type of message map called a COMMAND_HANDLER, which assumes that the primary message is WM_COMMAND (see Listing 8.16) and breaks the submessage into its appropriate values for you. The function OnAlignmenComboSelChange simply sets the dirty flag for the property page and exits. The last step is the actual updating of the control's properties when the property page exits. By setting the dirty flag of the property page, you instruct the Apply function to execute, which allows you the opportunity to update all the property values that the property page is responsible for. The Apply function is already supplied to you as part of the property page implementation (see Listing 8.17). You need only add your support for your specific properties. Since more than one object may have a reference to the property page, you are required to notify all of the objects that the properties have changed. The Apply function cycles through all of the references checking to see whether each contains the interface that you are looking for. If you find an interface that you can use, you then need only call the appropriate member functions for each of the properties that you support. If, for some reason, the setting of the property causes an error, the property page notifies the user with an error message. You then reset the m_bDirty flag to FALSE and exit the function. At this point, the property dialog closes, and control returns to the development environment hosting the control.
Listing 8.17 ATLCONTROLWINPPG.H--Modify the Apply Function to Update All of the Properties in the Control When the Property Page Exits . . . STDMETHOD(Apply)(void) { USES_CONVERSION; ATLTRACE(_T("CATLControlWinPPG::Apply\n")); for (UINT i = 0; i < m_nObjects; i++) { // see if the object supports the interface we need CComQIPtr pControl(m_ppUnk[i]); // get the current selection in the listbox long lAlignment = (long) ::SendMessage( ::GetDlgItem(m_hWnd, IDC_ALIGNMENTCOMBO), CB_GETCURSEL, 0, 0); // set the control with the new value and if it failed if FAILED(pControl->put_Alignment(lAlignment)) { // generate an error message CComPtr pError; CComBSTR strError; GetErrorInfo(0, &pError); pError->GetDescription(&strError); MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION); return E_FAIL; } } m_bDirty = FALSE; return S_OK; } . . . The property page implementation is complete; however, the control is still not aware of the property page's existence and that the control and the property page are related. To connect the property page to the control, you must add an entry to the BEGIN_PROPERTY_MAP macro in the class definition of the control (see Listing 8.18). The macro PROP_ENTRY is used in place of the PROP_PAGE macro so that the property will be persisted automatically when the control is saved.
Listing 8.18 ATLCONTROLWIN.H--Add the Property Page Reference to the Control Class Declaration to Complete the Property Page Implementation . . . BEGIN_PROPERTY_MAP(CATLControlWin) // PROP_ENTRY("Description", dispid, clsid) PROP_ENTRY("Alignment", dispidAlignment, CLSID_ATLControlWinPPG) PROP_PAGE(CLSID_CColorPropPage) END_PROPERTY_MAP() . . .
Adding Events Properties and methods are a way for a programmer to communicate with a control from within the container application. Events are a way for the control to communicate with the container. For ActiveX controls, events are nothing more than IDispatch interfaces that are implemented on the container side of the container/control relationship. The mechanism that events are based on is known as a connection point. A connection point is simply a description of the type of interface that is required in order to communicate with the container. Connection points are not restricted to only IDispatch interfaces; rather, they can be of any COM interface that is understood by both components. For that matter, connection points/events are not restricted only to controls; they can be used in any COM implementation. Controls were simply the first to take advantage of them. For more information regarding connection points, refer to the documentation in the OLE online help or to Kraig Brockschmidt's Inside OLE, Second Edition, published by Microsoft Press. Because no ClassWizards are available to aid you, adding events in ATL requires a bit more work than is required in MFC and BaseCtl. The first step is to add the event interface to your IDL file (see Listing 8.19). Remember to create a new UUID with the GUIDGEN.EXE program; do not reuse the UUIDs in the sample. In addition to the event interface, you need an event method. You also need to add the method called Change to the event interface. The Change method has two parameters. The first parameter is a string called bstrCaption, passed by reference (BSTR *). The second is a long called lAlignment, also passed by reference (long *). You are passing the parameters by reference to allow the container application the opportunity to change the values if necessary. You must also add the event interface to the CoClass interface for the control as the default, source interface.
Listing 8.19 ATLCONTROL.IDL--Add the Event Interface and the Change Method to the ATLControl.IDL File . . . [ uuid(C31D4C71-7AD7-11d0-BEF6-00400538977D), helpstring("ATLControlWin Event Interface") ] dispinterface _DATLControlWin { properties: methods: [id(1)] void Change([in, out]BSTR * bstrCaption, [in, out] long * lAlignment); }; [ uuid(A19F6964-7884-11D0-BEF3-00400538977D), helpstring("ATLControlWin Class") ] coclass ATLControlWin { [default] interface IATLControlWin; [default, source] dispinterface _DATLControlWin; }; . . . Event interfaces are based on connection point interfaces, which you will add to your control implementation.
Connection points are implemented with the IConnectinPoint and the IConnectionPointContainer interfaces. To aid in the creation of the interfaces, ATL provides a proxy generator. To generate the proxy code follow these steps: 1. From the Project menu, select the Add to Project menu item, and then select the Com_ponents and Controls submenu item. 2. In the Components and Controls Gallery dialog, double-click the Developer Studio Components folder. 3. After the Components and Controls Gallery dialog is refreshed with data, double-click the ATL Proxy Generator icon (see fig. 8.18). FIG. 8.18 Select the ATL Proxy Generator from the Developer Studio Components. 4. Click OK to close the Insert the ProxyGen Component dialog. 5. Click the ... button, in the ATL Proxy Generator dialog to display the Open dialog. Select the ATLControl.tlb file, and click Open. 6. Select the _DATLControlWin entry in the Not Selected list box, and click the > button to move the entry to the Selected list box (see fig. 8.19). Ensure that the Proxy Type is set to Connection Point, and click Insert. FIG. 8.19 Select the _DATLControlWin class in the ATL Proxy Generator dialog. 7. A Save dialog appears with the file CPATLControl.h in the File name edit box. Click Save to continue. Click OK in the confirmation dialog that indicates that the operation was successful. 8. Click Close in the ATL Proxy Generator and Components and Controls Gallery dialogs. The file CPATLControl.h contains that class CProxy_DATLControlWin, which implements your event interface, and the Change event method in the form Fire_Change, which you added earlier to the IDL file. The next step is to add the CProxy_DATLControlWin interface to your control implementation. Add the CPATLControl.h file as an include file to your ATLControlWin.h file: #include "CPATLControl.h You must also update the inheritance structure of the CATLControlWin class with the CProxy_DATLControlWin class public CProxy_DATLControlWin and add IID of the event interface to the IProvideClassInfo2Impl interface public IProvideClassInfo2Impl
See the ATL and ActiveX documentation for information regarding the implementation of the IProvideClassInfo2 interface. Last you add the event interface to the BEGIN_CONNECTION_POINT_MAP macro in the control's class declaration: BEGIN_CONNECTION_POINT_MAP(CATLControlWin) CONNECTION_POINT_ENTRY(DIID__DATLControlWin) END_CONNECTION_POINT_MAP() Your event interface is now completely implemented. The last step is only a matter of adding the Fire_Change method calls wherever appropriate in your control implementation. Since your implementation of the Fire_Change method allows the user of the control to change the data that is passed to the event, using a universal helper function is easier to maintain the code by implementing a simple helper function, FireChange (with no parameters), that encapsulates the data management associated with the method and its parameters (see Listing 8.20). Remember to add the FireChange function prototype to your class definition in the ATLCONTROLWIN.H header file.
Listing 8.20 ATLCONTROLWIN.CPP--FireChange Helper Function Added to the Control
void CATLControlWin::FireChange(void) { // needed for the W2A macro USES_CONVERSION; // get a BSTR that can be passed via the event BSTR bstrCaption = ::SysAllocString(T2OLE(m_lptstrCaption)); // fire the change event this->Fire_Change(&bstrCaption, &m_lAlignment); // convert the string to ANSI LPTSTR lptstrTempCaption = W2A(bstrCaption); // free the data that was passed back ::SysFreeString(bstrCaption); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lptstrTempCaption) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lptstrTempCaption); } Finally your event code is completely implemented. The common FireChange function allows you to hide the details surrounding the change event from the rest of the program. If you decide to change the FireChange
implementation in the future, it will impact only one function rather than a number of them. The CaptionMethod will require that you fire a Change event if the data changes, so you add your new event to the CaptionMethod (see Listing 8.21). You also want to add the FireChange event to the put_Alignment function, but do not add FireChange to the put_CaptionProp function because it defers to the CaptionMethod for its implementation. Also, do not forget to add the FireChange call to any new functions that are added to the control as its implementation progresses.
Listing 8.21 ATLCONTROLWIN.CPP--FireChange Event Added to the CaptionMethod Implementation
STDMETHODIMP CATLControlWin::CaptionMethod(BSTR bstrCaption, VARIANT varAlignment, long * lRetVal) { . . . // if everything was OK if(TRUE == *lRetVal) { // if we have a string if(lptstrTempCaption != NULL) { // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lptstrTempCaption) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lptstrTempCaption); } // did they pass us bad data? if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT) // sure did, lets fix their little red wagon m_lAlignment = EALIGN_LEFT; // fire the global change event this->FireChange(); // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); GetAmbientFont(&m_pFont); // if there still isn't a font object if(!m_pFont) // create a default font object ::OleCreateFontIndirect(&_fdDefault, IID_IFont, (void **) &m_pFont); } void CATLControlWin::GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy) { // if we haven't gotten the dimensions yet if(!bRetrievedDimensions) { // get all of the widths for all of the chars ::GetCharWidth(hDC, 0, 255, &iCharWidthArray[0]); // get the spacing between the chars iCharacterSpacing = ::GetTextCharacterExtra(hDC); // make sure that this only executes once bRetrievedDimensions = TRUE; // get the metrics of this DC TEXTMETRIC tmMetrics; ::GetTextMetrics(hDC, &tmMetrics); // get the height iCharacterHeight = tmMetrics.tmHeight; } // return the height cy = iCharacterHeight; // set the initial value to 0 int iTextWidth = 0; // get the number of characters in our string long lTextLength = lstrlen(lpctstrString);
// if we have a character if(lTextLength) { long lEndCharPos = lTextLength - 1; // add up the widths of the characters and the spacing for(long lCount = 0; lCount AddRefHfont(hFont); ::SelectObject(di.hdcDraw, hFont); } // ** // ****** Get the text font ****** // ****** Get the colors ****** // ** // use the window color as the background color OLE_COLOR tColor; this->get_BackColor(&tColor); COLORREF clrTextBackgroundColor = this->TranslateColor(tColor); // then use the normal windows color for the text COLORREF clrTextForegroundColor = this->TranslateColor(::GetSysColor(COLOR_WINDOWTEXT)); // set to the system color COLORREF clrEdgeBackgroundColor = ::GetSysColor(COLOR_3DFACE);
COLORREF clrEdgeForegroundColor = ::GetSysColor(COLOR_3DFACE); // ** // ****** Get the colors ****** // ****** Draw the background ****** // ** // set the text color COLORREF clrOldBackgroundColor = ::SetBkColor(di.hdcDraw, clrTextBackgroundColor); COLORREF clrOldForegroundColor = ::SetTextColor(di.hdcDraw, clrTextForegroundColor); // if we don't have a brush if(hBrush == NULL) // create a solid brush hBrush = ::CreateSolidBrush(clrTextBackgroundColor); // select the brush and save the old one hOldBrush = (HBRUSH)::SelectObject(di.hdcDraw, hBrush); // draw the background ::Rectangle(di.hdcDraw, di.prcBounds->left, di.prcBounds->top, di.prcBounds->right, di.prcBounds->bottom); // ** // ****** Draw the background ****** // ****** Draw the text ****** // ** int iHor, iVer; // get the size of the text for this DC int cx = 0, cy = 0; this->GetTextExtent(di.hdcDraw, m_lptstrCaption, cx, cy); switch(m_lAlignment) { case EALIGN_CENTER: iHor = (di.prcBounds->right - cx) / 2; iVer = di.prcBounds->top + 3; break; case EALIGN_RIGHT: iHor = di.prcBounds->right - cx - 3; iVer = di.prcBounds->top + 3; break; // case EALIGN_LEFT: default: iHor = di.prcBounds->left + 3; iVer = di.prcBounds->top + 3; break; } // output our text ::ExtTextOut(di.hdcDraw, iHor, iVer, ETO_CLIPPED | ETO_OPAQUE, (LPCRECT) di.prcBounds, m_lptstrCaption, lstrlen(m_lptstrCaption), NULL); // ** // ****** Draw the text ****** // ****** Draw the border ****** // ** // set the edge style and flags UINT uiBorderStyle = EDGE_SUNKEN;
UINT uiBorderFlags = BF_RECT; // set the edge color ::SetBkColor(di.hdcDraw, clrEdgeBackgroundColor); ::SetTextColor(di.hdcDraw, clrEdgeForegroundColor); // draw the 3D edge ::DrawEdge(di.hdcDraw, (LPRECT)(LPCRECT) di.prcBounds, uiBorderStyle, uiBorderFlags); // ** // ****** Draw the border ****** // ****** Reset the colors ****** // ** // restore the original colors ::SetBkColor(di.hdcDraw, clrOldBackgroundColor); ::SetTextColor(di.hdcDraw, clrOldForegroundColor); // ** // ****** Reset the colors ****** // ****** release the text font ****** // ** if(hOldFont) // select the old object ::SelectObject(di.hdcDraw, hOldFont); // increment the ref count so the font doesn't drop // out from under us if(m_pFont && hFont) m_pFont->ReleaseHfont(hFont); // ** // ****** Get the text font ****** // select the old brush back ::SelectObject(di.hdcDraw, hOldBrush); // destroy the brush we created ::DeleteObject(hBrush); // clear the brush handles hBrush = hOldBrush = NULL; return S_OK; }
From Here... This chapter focused on creating a basic control implementation. You added methods, properties, and events, which are the backbone of every control implementation. This chapter also addressed the issues of persistence and drawing without which a control implementation is definitely incomplete. Version 2.1 has brought ATL into the realm of complete ActiveX development frameworks alongside MFC. The integration of ATL into the VC++ IDE was long overdue and sorely needed. Finally there is a competitor for MFC in terms of rapid ActiveX/COM development, with the added benefit of the ATL framework being lean and to the point. With ATL being so close to the COM interface level, you also have the added benefit of seeing how truly simple and easy it is to implement COM interfaces and components. The only drawbacks to ATL might be its support of events and some of its tools for creating components. We are certain, however, that ATL will continue to mature and be the premier ActiveX development framework available.
Chapter 9 expands upon the knowledge you gained in this chapter and adds new features and function to your control implementation to make your control truly unique and interesting.
Chapter 9 Advanced ActiveX Control Development with ATL ●
Advanced ActiveX Control Development with ATL ❍ Properties ■ Creating Asynchronous Properties ■ Listing 9.1 ATLCONTROL.IDL--Change the dispid of the ReadyState Property to the Stock Property dispid--DISPID_READYSTATE ■ Listing 9.2 ATLCONTROLWIN.H--Add the m_lReadyState Member to the CATLControlWin Class ■ Listing 9.3 ATLCONTROLWIN.H--Initialize the m_lReadyState Member Variable in the Class Constructor ■ Listing 9.4 ATLCONTROLWIN.CPP--Implement the get_ReadyState Function to Return the Current ReadyState Value ■ Listing 9.5 ATLCONTROL.IDL--Add the ReadyStateChange Event to the IDL File ■ Listing 9.6 ATLCONTROL.IDL--Add the dispidTextDataPath Enumeration to the IDL File ■ Listing 9.7 ATLCONTROLWIN.H--The m_bstrTextDataPath Member Variable Is Added to the Class Declaration to Store the TextDataPath Property ■ Listing 9.8 ATLCONTROLWIN.CPP--Implementation of the get_TextDataPath /put_TextDataPath Functions ■ Listing 9.9 ATLCONTROLWIN.CPP--OnData Function Implementation ■ Listing 9.10 ATLCONTROLWIN.H--TextDataPath Member Added to the Property Persistence Macro ■ Static and Dynamic Property Enumeration ■ Listing 9.11 ATLCONTROLWIN.H--IPerPropertyBrowsing Interface Function Prototypes Must Be Added to the Class Declaration ■ Listing 9.12 ATLCONTROLWIN.CPP--MapPropertyToPage Implementation ■ Listing 9.13 ATLCONTROLWIN.CPP--GetPredefinedStrings Implementation ■ Listing 9.14 ATLCONTROLWIN.CPP--GetPredefinedValue Implementation ■ Listing 9.15 ATLCONTROLWIN.CPP--GetDisplayString Implementation ❍ Drawing the Control ■ Optimized Drawing ■ Listing 9.16 ATLCONTROLWIN.H--Drawing Implementation Member Variables and Functions ■ Listing 9.17 ATLCONTROLWIN.CPP--OnDestroy Implementation of Drawing Resource Cleanup ■ Listing 9.18 ATLCONTROLWIN.CPP--OnDraw Function Updated to Support Optimized Drawing ❍ Adding Clipboard and Drag and Drop Support ■ Clipboard Support ■ Listing 9.19 ATLCONTROLWIN.H--WM_KEYDOWN and OnKeyDown Message Handler Added to the Class Declaration of the Control ■ Listing 9.20 ATLCONTROLWIN.CPP--OnKeyDown Implementation
Listing 9.21 ATLCONTROLWIN.H--Helper Functions and Member Variables for Clipboard Support ■ Listing 9.22 ATLCONTROLWIN.H--IEnumFORMATETC Interface Added to theClass Inheritance Hierarchy ■ Listing 9.23 ATLCONTROLWIN.H--Member Initialization in the Class Constructor ■ Listing 9.24 ATLCONTROLWIN.CPP--CopyDataToClipboard Helper Function Implementation ■ Listing 9.25 ATLCONTROLWIN.CPP--PrepareDataForTransfer Helper Function Implementation ■ Listing 9.26 ATLCONTROLWIN.CPP--CopyStgMedium Helper Function Implementation ■ Listing 9.27 ATLCONTROLWIN.CPP--IDataObject Interface Implementation ■ Listing 9.28 ATLCONTROLWIN.CPP--IEnumFORMATETC Interface Implementation ■ Listing 9.29 ATLCONTROLWIN.H--Clipboard Target Support Helper Function Prototypes ■ Listing 9.30 MFCCONTROLWINCTL.CPP--GetDataFromClipboard Implementation ■ Listing 9.31 MFCCONTROLWINCTL.CPP--GetDataFromTransfer Implementation ■ Listing 9.32 ATLCONTROLWIN.CPP--OnKeyDown Implementation ■ Drag and Drop Support ■ Listing 9.33 ATLCONTROLWIN.H--IDropSource Interface Added to the CATLControlWin Class Declaration ■ Listing 9.34 ATLCONTROLWIN.H--WM_LBUTTONDOWN and OnLButtonDown Message Handler Added to the Class Declaration of the Control ■ Listing 9.35 ATLCONTROLWIN.CPP--OnLButtonDown Implementation ■ Listing 9.36 ATLCONTROLWIN.CPP--IDropSource Implementation ■ Listing 9.37 ATLCONTROLWIN.H--IDropTarget Interface Added to the CATLControlWin Class Declaration ■ Listing 9.38 ATLCONTROLWIN.H--WM_CREATE Message Handler ■ Listing 9.39 ATLCONTROLWIN.CPP--OnCreate Implementation ■ Listing 9.40 ATLCONTROLWIN.CPP--OnDestroy Implementation Updated to Revoke the Control as a Valid Drop Target ■ Listing 9.41 ATLCONTROLWIN.CPP--IDropTarget Interface Implementation ■ Custom Clipboard and Drag and Drop Formats ■ Listing 9.42 ATLCONTROLWIN.H--Custom Data Format Member Variables ■ Listing 9.43 ATLCONTROLWIN.H--Register the Custom Format and Initialize the Member Variables in the Class Constructor ■ Listing 9.44 ATLCONTROLWIN.CPP--PrepareDataForTransfer Update ■ Listing 9.45 ATLCONTROLWIN.CPP--GetDataFromTransfer Update ■ Listing 9.46 ATLCONTROLWIN.CPP--IEnumFORMATETC::Next Update ■ Listing 9.47 TLCONTROLWIN.CPP--IEnumFORMATETC::GetData Update Subclassing Existing Windows Controls ■ Listing 9.48 ATLCONTROLSUBWIN.H--CATLControlSubWin Class Implementation Dual-Interface Controls Other ActiveX Features ■ Windowless Activation ■ Flicker-Free Activation ■ Mouse Pointer Notifications When Inactive ■ Optimized Drawing Code ■ Loads Properties Asynchronously ■
❍
❍ ❍
❍
From Here...
Advanced ActiveX Control Development with ATL ●
Asynchronous properties ATL hides some of the implementation details making implementation easier.
●
Property enumeratio Property enumeration allows you to restrict the set of values a property can contain and makes the property appear more professional in its implementation. ●
●
●
●
●
Optimized drawing Optimized drawing with ATL is easy and can have positive effects on the performance and appearance of the control. Clipboard and Drag and Drop With ATL, you can use a set of routines that includes support for custom data types. Subclassing Windows controls Subclassing an existing Windows control with ATL can significantly reduce your development time when creating new controls. Dual-interface controls Unlike MFC, ATL ActiveX controls are dual-interface by default and require no extra work to implement. Advanced ActiveX The ATL framework provides advanced features by default.
This chapter expands upon the information in Chapter 8 about creating a basic ATL ActiveX control. In addition to the features that you are familiar with, such as Clipboard and Drag and Drop support, you will learn how to implement asynchronous properties and optimized drawing, which are the result of the adoption of OLE Control 96 (OC 96) specification.
Properties Properties In Chapter 8, you learn how to add the various types of properties to your control implementation. One type of property that has yet to be examined in terms of ATL is asynchronous properties.
Creating Asynchronous Properties
Asynchronous properties are those properties that typically represent a large amount of data, such as a text file or a bitmap, and are loaded as a background process so as not to interfere with the normal processing of the control and the container. This statement can be somewhat misleading. Asynchronous refers only to the call to load the data; it does not refer to the actual loading. For example, a control uses a bitmap as its background and has defined the bitmap as an asynchronous property. If OLE determines that the bitmap is already on the local machine, the data is considered to be available to the control and, subsequently, will instruct the control that all of the data is available. If OLE determines that the bitmap is not available on the local machine, OLE will load the data as fast as possible and inform the control as data becomes available. After the data is in a location that is considered accessible, the property essentially behaves as any other property would. If you require the asynchronous loading of the data regardless of its location, you must implement it yourself. Before you can add your asynchronous property, you need to add the property ReadyState, which is used by the container to determine the state that the control is in at any given time relative to the loading of asynchronous properties. You also add the event ReadyStateChange, which is used by the control to notify the container that the ReadyState property of the control has changed. Adding the ReadyState property is the same as adding any other property, as is described in Chapter 8. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface, click the right mouse button, and select the Add Property menu item. In the Add Property to Interface dialog, set the Property Type to long, the Property Name to ReadyState, uncheck the Put Function check box, and leave the remainder of the settings at their default values (see fig. 9.1). Click OK to confirm the entry, and close the dialog. Before you proceed, open the ATLControl.idl file, and change the dispid of the ReadyState property to DISPID_READYSTATE since ReadyState is a stock property (see Listing 9.1). FIG. 9.1 Add the ReadyState property to the control using the ATL Object Wizard for your asynchronous property support.
Listing 9.1 ATLCONTROL.IDL--Change the dispid of the ReadyState Property to the Stock Property dispid--DISPID_READYSTATE . . . interface IATLControlWin : IDispatch { [id(1), helpstring("method CaptionMethod")] HRESULT CaptionMethod( [in] BSTR bstrCaption, [in, optional] VARIANT varAlignment, [out, retval] long * lRetVal); [propget, id(DISPID_READYSTATE), helpstring("property ReadyState")] HRESULT ReadyState([out, retval] long *pVal); [propget, id(DISPID_BACKCOLOR), helpstring("property BackColor")] HRESULT BackColor([out, retval] OLE_COLOR *pVal); [propput, id(DISPID_BACKCOLOR), helpstring("property BackColor")] HRESULT BackColor([in] OLE_COLOR newVal); [propget, id(dispidCaptionProp), helpstring("property CaptionProp")] HRESULT CaptionProp([in, optional] VARIANT varAlignment, [out, retval] BSTR *pVal);
[propput, id(dispidCaptionProp), helpstring("property CaptionProp")] HRESULT CaptionProp([in, optional] VARIANT varAlignment, [in] BSTR newVal); [propget, id(dispidAlignment), helpstring("property Alignment")] HRESULT Alignment([out, retval] long *pVal); [propput, id(dispidAlignment), helpstring("property Alignment")] HRESULT Alignment([in] long newVal); }; . . . The implementation of the ReadyState property requires a member variable to store the ReadyState value. Add the m_lReadyState member to the class declaration of the CATLControlWin class (see Listing 9.2).
Listing 9.2 ATLCONTROLWIN.H--Add the m_lReadyState Member to the CATLControlWin Class . . . int iCharWidthArray[256]; int iCharacterSpacing, iCharacterHeight; // for the ReadyState property long m_lReadyState; }; Add the initialization of the m_lReadyState member variable to the constructor of the CATLControlWin class (see Listing 9.3).
Listing 9.3 ATLCONTROLWIN.H--Initialize the m_lReadyState Member Variable in the Class Constructor CATLControlWin() { . . . // set the initial state of the ReadyState property m_lReadyState = READYSTATE_LOADING; } The last step to implement is the get_ReadyState function, which simply returns the current value of the m_lReadyState member variable (see Listing 9.4).
Listing 9.4 ATLCONTROLWIN.CPP--Implement the get_ReadyState Function to Return the Current ReadyState Value STDMETHODIMP CATLControlWin::get_ReadyState(long * pVal) { // set the return value to the value of the member variable *pVal = m_lReadyState; return S_OK; }
The next step is to add support for the ReadyStateChange event. Open the ATLControl.idl file, and add the ReadyStateChange function to the event interface that is added in Chapter 8 (see Listing 9.5).
Listing 9.5 ATLCONTROL.IDL--Add the ReadyStateChange Event to the IDL File . . . [ uuid(C31D4C71-7AD7-11d0-BEF6-00400538977D), helpstring("ATLControlWin Event Interface") ] dispinterface _DATLControlWin { properties: methods: [id(1)] void Change([in, out]BSTR * bstrCaption, [in, out] long * lAlignment); [id(DISPID_READYSTATECHANGE)] void ReadyStateChange(); }; . . . Remember that support for events is not automatic in ATL, so you must manually rebuild the CPATLControl.h file that was created in Chapter 8 for your connection point support by using the ATL Proxy Generator. To update the file follow these steps: 1. Compile the IDL file since the event interface header file is built from the type library. 2. From the Project menu, select the Add to Project menu item, and then select the Components and Controls submenu item. 3. In the Components and Controls Gallery dialog, double-click the Developer Studio Components folder. 4. After the Components and Controls Gallery dialog is refreshed with data, double-click the ATL Proxy Generator icon. 5. Click OK to close the Insert the ProxyGen Component dialog. 6. Click the ... button to display the Open dialog. Select the ATLControl.tlb file, and click Open. 7. Select the _DATLControlWin entry in the Not Selected list box, and click the > button to move the entry to the Selected list box. Ensure that the Proxy Type is set to Connection Point and click Insert. 8. A Save dialog appears with the file CPATLControl.h in the File name edit box. Click Save to continue. Click Yes to replace the existing CPATLControl.h file. 9. Click OK in the confirmation dialog that indicates the operation was successful. 10. Click Close in the ATL Proxy Generator and Components and Controls Gallery dialogs.
The Fire_ReadyStateChange method is now added to the CProxy_DATLControlWin class. Asynchronous properties are based on URLs and not on the data type of the data to be downloaded, for example, a bitmap or text file. The URL is stored in a string property of the control. For the sample implementation, you add the property called TextDataPath to the control. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface, click the right mouse button, and select the Add Property... menu item. In the Add Property to Interface dialog, set the Property Type to BSTR, the Property Name to TextDataPath, and leave the remainder of the settings at their default values (see fig. 9.2). Click OK to confirm the entry and close the dialog. FIG. 9.2 Add the TextDataPath property to the control using the ATL ClassWizard. Add the dispidTextDataPath constant to the PROPDISPIDS enumeration in the ATLControl.idl file, and update the TextDataPath function to use the constant value (see Listing 9.6).
Listing 9.6 ATLCONTROL.IDL--Add the dispidTextDataPath Enumeration to the IDL File . . . typedef enum propdispids { dispidAlignment = 2, dispidCaptionProp = 3, dispidTextDataPath = 4, }PROPDISPIDS; [ object, uuid(A19F6963-7884-11D0-BEF3-00400538977D), dual, helpstring("IATLControlWin Interface"), pointer_default(unique) ] interface IATLControlWin : IDispatch { [id(1), helpstring("method CaptionMethod")] HRESULT CaptionMethod( [in] BSTR bstrCaption, [in, optional] VARIANT varAlignment, [out, retval] long * lRetVal); [propget, id(dispidTextDataPath), helpstring("property TextDataPath")] HRESULT TextDataPath([out, retval] BSTR *pVal); [propput, id(dispidTextDataPath), helpstring("property TextDataPath")] HRESULT TextDataPath([in] BSTR newVal); . . . The TextDataPath property is used to store the URL of the data that the property represents. To complete your implementation of the property, add the member variable, m_bstrTextDataPath (see Listing 9.7). The data for the member is declared as the type CComBSTR, which is a BSTR wrapper class provided with ATL. See the ATL documentation for more information. The use of the CComBSTR data type versus a standard BSTR or LPTSTR is purely an arbitrary decision on your part and is based on your implementation requirements. We used
CComBSTR to demonstrate the different implementation styles available to you with ATL. You also add the member variable m_bstrText, also of the type CComBSTR, to store the data as it is supplied to the control and the member function OnData, which will be the callback function that receives the data as it is downloaded. We will discuss these two members a little later in this chapter.
Listing 9.7 ATLCONTROLWIN.H--The m_bstrTextDataPath Member Variable Is Added to the Class Declaration to Store the TextDataPath Property . . . //OnData will be used as a callback functin by the CBindStatusCallback object. //OnData will be called periodically with data from the asynchronous transfer void OnData(CBindStatusCallback* pbsc, BYTE* pBytes, DWORD dwSize); protected: . . . // for the ReadyState property long m_lReadyState; // for the TextDataPath property CComBSTR m_bstrTextDataPath; // to hold the data as it is passed in CComBSTR m_bstrText; }; #endif //__ATLCONTROLWIN_H_ The implementation of the get_TextDataPath/put_TextDataPath function is where the asynchronous data transfer of the property takes place (see Listing 9.8). The get_TextDataPath function returns the current value stored in the m_bstrTextDataPath member variable. The put_TextDataPath function stores the new location of the data and then initiates a transfer of the data to the control with a call to CBindStatusCallback::Download (. . .). CBindStatusCallback is an ATL wrapper class that wraps the IBindStatusCallback interface. CBindStatusCallback handles all of the details of the data transfer and only requires that you implement a function, in this case OnData, to receive the data as it is downloaded. The OnData function is supplied as the second parameter to the Download function and must conform to the prototype defined by ATL. See the ATL documentation on the Download function for more information.
Listing 9.8 ATLCONTROLWIN.CPP--Implementation of the get_TextDataPath /put_TextDataPath Functions STDMETHODIMP CATLControlWin::get_TextDataPath(BSTR * pVal) { // return a copy of the member variable *pVal = m_bstrTextDataPath.Copy(); return S_OK; } STDMETHODIMP CATLControlWin::put_TextDataPath(BSTR newVal) { HRESULT hResult = S_OK; // copy the new string to the member variable
m_bstrTextDataPath = newVal; // clear the data buffer m_bstrText = _T(""); // start the asynchronous download of the data CBindStatusCallback::Download(this, OnData, m_bstrTextDataPath, m_spClientSite, FALSE); // let the container know that the property has changed this->SetDirty(TRUE); // this->SetModifiedFlag(); m_dwTotalRead == 0) { // clear the buffer m_bstrText = _T(""); // set the ready state of the control m_lReadyState = READYSTATE_LOADING; // let the container know that the property has changed this->Fire_ReadyStateChange(); } // add the data to our buffer m_bstrText.Append((LPCSTR) pBytes); long lRetVal; VARIANT varAlignment; // initialize the variant ::VariantInit(&varAlignment); // defer to the CaptionMethod implementation this->CaptionMethod(m_bstrText, varAlignment, &lRetVal);
// if the function returned success if(TRUE == lRetVal) // let the control know that the property has changed this->SetDirty(TRUE); // this->SetModifiedFlag(); m_dwAvailableToRead == 0) { // set the ready state of the control m_lReadyState = READYSTATE_COMPLETE; // let the container know that the property has changed this->Fire_ReadyStateChange(); } } The final touch of your asynchronous property implementation is to add the property to the persistence macro in your class declaration (see Listing 9.10). Do not add the ReadyState property to the persistence since its value is not valid across execution lifetimes.
Listing 9.10 ATLCONTROLWIN.H--TextDataPath Member Added to the Property Persistence Macro . . . BEGIN_PROPERTY_MAP(CATLControlWin) // PROP_ENTRY("Description", dispid, clsid) PROP_ENTRY("TextDataPath", dispidTextDataPath, CLSID_ATLControlWinPPG) PROP_ENTRY("Alignment", dispidAlignment, CLSID_ATLControlWinPPG) PROP_ENTRY("BackColor", DISPID_BACKCOLOR, CLSID_ATLControlWinPPG) PROP_PAGE(CLSID_CColorPropPage) END_PROPERTY_MAP() . . .
Static and Dynamic Property Enumeration Property enumeration is a way of restricting a property to a specific set of valid values. An example of an enumeration is a property for determining the alignment of a control's displayed text: left-justified, centered, and right-justified, in your case. Another case is a property used to select the different languages a control supports. A language-based property is a good candidate for both a static set, say English and German, and a dynamic set, say for all the languages on a particular machine. As is pointed out in Chapter 7, property enumeration adds a new level of sophistication to your control with very little effort. You can take two approaches when creating an enumeration for a property: use a static approach with an enumeration defined in the control's ODL or IDL file, or use a dynamic approach with enumeration code implemented in the control itself. Static Property Enumeration Static property enumeration for an ATL-implemented control is no different than the MFC implementation. Static enumeration is dependent on the ODL/IDL file and involves no control code to implement. See Chapter 7 for more information.
Dynamic Property Enumeration As with your MFC implementation, adding dynamic property enumeration to your ATL implementation is straightforward. Dynamic property enumeration is based on the interface IPerPropertyBrowsing. The ATL Object Wizard does not add this interface to the control class automatically; you are required to add it yourself. The first step is to add the IPerPropertyBrowsingImpl class to your control inheritance structure: public IPerPropertyBrowsingImpl And then add the IPerPropertyBrowsing interface to the COM interface map: COM_INTERFACE_ENTRY_IMPL(IPerPropertyBrowsing) The last step is to implement the IPerPropertyBrowsing interface functions. First you add the function prototypes to the class declaration (see Listing 9.11).
Listing 9.11 ATLCONTROLWIN.H--IPerPropertyBrowsing Interface Function Prototypes Must Be Added to the Class Declaration . . . STDMETHOD(MapPropertyToPage)(DISPID dispID, CLSID *pClsid); STDMETHOD(GetPredefinedStrings)(DISPID dispID, CALPOLESTR *pCaStringsOut, CADWORD *pCaCookiesOut); STDMETHOD(GetPredefinedValue)(DISPID dispID, DWORD dwCookie, VARIANT* pVarOut); STDMETHOD(GetDisplayString)(DISPID dispID,BSTR *pBstr); . . . MapPropertyToPage (see Listing 9.12) is used to identify a property to a specific control or system-defined property page. In this case, you return E_NOTIMPL if the dispid matches that of the Alignment property. By returning E_NOTIMPL, you are preventing the container application from displaying the property page associated with the property; instead, the container will use the property enumeration that you have implemented. The connection between the property and the property page is made in the property map macro in the class declaration. Remember that one of the parameters in the macro was the CLSID of the property page for the property.
Listing 9.12 ATLCONTROLWIN.CPP--MapPropertyToPage Implementation STDMETHODIMP CATLControlWin::MapPropertyToPage(DISPID dispID, CLSID *pClsid) { // if this is the dispid property if(dispID == dispidAlignment) // defer to the property enumeration and not the property page return E_NOTIMPL; else // defer to the base class implementation return IPerPropertyBrowsingImpl:: MapPropertyToPage(dispID, pClsid); } GetPredefinedStrings is the first function to be called (see Listing 9.13). When this method is called, the
dispid of the property that is currently being referenced will be passed in. This method is called for all properties that the control supports, so take care when adding code. If the function is called and it is determined that the correct property is in context, the control is required to create an array of strings and cookies. A cookie is any 32-bit value that has meaning to the control implementation. In this case, the cookies that you supply are the enumeration constants that you added in Chapter 8. The strings are placed in a list from which the user of the control can select the appropriate value to set the property.
Listing 9.13 ATLCONTROLWIN.CPP--GetPredefinedStrings Implementation STDMETHODIMP CATLControlWin::GetPredefinedStrings(DISPID dispid, CALPOLESTR * lpcaStringsOut, CADWORD * lpcaCookiesOut) { USES_CONVERSION; HRESULT hResult = S_FALSE; // we should have gotten two pointers if we didn't if((lpcaStringsOut == NULL) || (lpcaCookiesOut == NULL)) // we are out of here return E_POINTER; // if this is the property that we are looking for if(dispid == dispidAlignment) { ULONG ulElems = 3; // allocate the memory for our string array lpcaStringsOut->pElems = (LPOLESTR *) ::CoTaskMemAlloc( sizeof(LPOLESTR) * ulElems); // if we couldn't allocate the memory if(lpcaStringsOut->pElems == NULL) // were out of here return E_OUTOFMEMORY; // allocate the memory for our cookie array lpcaCookiesOut->pElems = (DWORD*) ::CoTaskMemAlloc(sizeof(DWORD*) * ulElems); // if we couldn't allocate the memory if (lpcaCookiesOut->pElems == NULL) { // free the string array ::CoTaskMemFree(lpcaStringsOut->pElems); // exit the function return E_OUTOFMEMORY; } // store the number of elements in each array lpcaStringsOut->cElems = ulElems; lpcaCookiesOut->cElems = ulElems; // allocate the strings lpcaStringsOut->pElems[0] = ATLA2WHELPER((LPWSTR)::CoTaskMemAlloc( (lstrlen(EALIGN_LEFT_TEXT) + 1) * 2), EALIGN_LEFT_TEXT, lstrlen(EALIGN_LEFT_TEXT) + 1); lpcaStringsOut->pElems[1] = ATLA2WHELPER((LPWSTR)::CoTaskMemAlloc( (lstrlen(EALIGN_CENTER_TEXT) + 1) * 2), EALIGN_CENTER_TEXT, lstrlen(EALIGN_CENTER_TEXT) + 1); lpcaStringsOut->pElems[2] = ATLA2WHELPER((LPWSTR)::CoTaskMemAlloc(
(lstrlen(EALIGN_RIGHT_TEXT) + 1) * 2), EALIGN_RIGHT_TEXT, lstrlen(EALIGN_RIGHT_TEXT) + 1); // assign the cookie value lpcaCookiesOut->pElems[0] = EALIGN_LEFT; lpcaCookiesOut->pElems[1] = EALIGN_CENTER; lpcaCookiesOut->pElems[2] = EALIGN_RIGHT; hResult = S_OK; } return hResult; } GetPredefinedValue (see Listing 9.14) is the method that is called when the property browser of the container needs the value associated with the particular dispid and cookie. The value returned is the actual value that is stored in the property and not the string that was used to represent it.
Listing 9.14 ATLCONTROLWIN.CPP--GetPredefinedValue Implementation STDMETHODIMP CATLControlWin::GetPredefinedValue(DISPID dispid, DWORD dwCookie, VARIANT* lpvarOut) { BOOL bResult = FALSE; // which property is it switch(dispid) { case dispidAlignment: // clear the variant ::VariantInit(lpvarOut); // set the type to a long lpvarOut->vt = VT_I4; // set the value to the value that was stored with the string lpvarOut->lVal = dwCookie; // set the return value bResult = TRUE; break; } return bResult; } After the property is set with its value, the property browser calls the function GetDisplayString to get the string that is associated with the current property setting (see Listing 9.15).
Listing 9.15 ATLCONTROLWIN.CPP--GetDisplayString Implementation STDMETHODIMP CATLControlWin::GetDisplayString(DISPID dispid, BSTR* lpbstr) { USES_CONVERSION; HRESULT hResult = S_FALSE; // which property is it switch(dispid)
{ case dispidAlignment: { switch(m_lAlignment) { case EALIGN_LEFT: *lpbstr = ::SysAllocString(T2OLE(EALIGN_LEFT_TEXT)); break; case EALIGN_CENTER: *lpbstr = ::SysAllocString(T2OLE(EALIGN_CENTER_TEXT)); break; case EALIGN_RIGHT: *lpbstr = ::SysAllocString(T2OLE(EALIGN_RIGHT_TEXT)); break; } // set the return value hResult = S_OK; } break; } return hResult; }
NOTE: Having the method GetDisplayString when the property browser already has the string for the value from the GetPredefinedStrings function may seem a little redundant. You do this because the GetDisplayString function can be implemented without implementing the other methods. Providing only the function GetDisplayString is done for those property types that do not use the standard property selection mechanism, for example, font selection that uses a font selection dialog and not a list of choices.
Drawing the Control Optimized drawing allows you to create drawing objects, such as pens or brushes. Rather than removing them when you are finished drawing, you can store them as control member variables and use them the next time your control draws itself. The benefit is that you create a pen once for the drawing lifetime of your control, instead of every time it draws. Here's one thing to remember, though: Optimized drawing does not guarantee performance improvements. It simply supplies a framework for how drawing should be performed and how drawing resources should be used. A poorly written control is still poorly written, no matter how you slice it. Standard and optimized drawings have a single tradeoff and that is size versus speed. Standard drawing does not require member variables for the drawing objects that are created and used-- thus requiring less instance data but more processing time--whereas optimized code is the opposite. An additional drawback to optimized drawing is that a container may not support it. A control must, at the very least, support standard drawing functionality, deferring to optimized only if it is available. For ATL (like MFC), the scope of optimized drawing is very narrow compared to the OC 96 specification, but it
will, nonetheless, result in performance improvements if taken advantage of. The OC 96 specification further breaks optimized drawing into what is known as aspects. For more information on aspect drawing, please see the OC 96 specification that ships with the ActiveX SDK.
Optimized Drawing In Chapter 8, you learn how to implement standard drawing. In this chapter, you enhance the original implementation to take advantage of drawing optimization. Add a message handler, OnDestroy, for the Windows message WM_DESTROY. Also add an OnDestroy function prototype to the class declaration (see Listing 9.16). OnDestroy is used to clean up the drawing resources if any are still around when the control is destroyed. This should happen only if the container supports optimized drawing.
Listing 9.16 ATLCONTROLWIN.H--Drawing Implementation Member Variables and Functions BEGIN_MSG_MAP(CATLControlWin) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode) MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus) MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() . . . LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled); . . . The next step is to add the OnDestroy implementation, which will clean up the resources if they are still allocated (see Listing 9.17).
Listing 9.17 ATLCONTROLWIN.CPP--OnDestroy Implementation of Drawing Resource Cleanup LRESULT CATLControlWin::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { // if there is an old brush if(hOldBrush) { // get the DC HDC hDC = this->GetDC(); // select the old brush back ::SelectObject(hDC, hOldBrush); // release the DC this->ReleaseDC(hDC); } // if we created a brush if(hBrush)
// destroy the brush we created ::DeleteObject(hBrush); return TRUE; } The last step is to update the OnDraw implementation to take advantage of optimized drawing, if the container supports optimized drawing that is (see Listing 9.18). The only line that you need to add to the code is one that checks the bOptimize member of the ATL_DRAWINFO structure to see if it is set to 0 (or FALSE), which indicates that the container does not support optimized drawing. If that is the case, you then clean up all of your allocated resources and restore any original values. If the container does support optimized drawing, you ignore the cleanup and reuse the allocated resources the next time around.
Listing 9.18 ATLCONTROLWIN.CPP--OnDraw Function Updated to Support Optimized Drawing HRESULT CATLControlWin::OnDraw(ATL_DRAWINFO & di) { . . . // The container does not support optimized drawing. if(!di.bOptimize) { // select the old brush back ::SelectObject(di.hdcDraw, hOldBrush); // destroy the brush we created ::DeleteObject(hBrush); // clear the brush handles hBrush = hOldBrush = NULL; } return S_OK; }
Adding Clipboard and Drag and Drop Support The basic OLE Clipboard and Drag and Drop interfaces are only partially implemented within your control implementation. As for the IPerPropertyBrowsing interface, you must implement the remaining required interfaces yourself. As is pointed out in Chapter 7, Clipboard and Drag and Drop support can add much to your control implementation while requiring very little work.
Clipboard Support Clipboard support is based on the IDataObject and IEnumFORMATETC interfaces. IDataObject is the interface through which the data is retrieved from your control when it has been placed on the Clipboard, and IEnumFORMATETC is the interface that is used by an application to determine what types of data your IDataObject interface supports. In a basic ATL control project, only the IDataObject Interface is supported. The IEnumFORMATETC interface must be added. Before adding the specific interfaces required by ActiveX to enable Clipboard transfers, you must first decide which keystroke combinations will be used to initiate the cut, copy, or paste operations. Fortunately, the Windows operating system (OS) already has a number of standards in this area. You use Ctrl+X and Shift+Delete for Cut,
Ctrl+C and Ctrl+Insert for Copy, and Ctrl+V and Shift+Insert for Paste. To trap the keystroke combinations, you need to implement a message handler for the WM_KEYDOWN message in the form of a method called OnKeyDown (see Listing 9.19).
Listing 9.19 ATLCONTROLWIN.H--WM_KEYDOWN and OnKeyDown Message Handler Added to the Class Declaration of the Control . . . BEGIN_MSG_MAP(CATLControlWin) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode) MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus) MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown) END_MSG_MAP() . . . LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled); LRESULT OnKeyDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled); . . . The OnKeyDown implementation looks for the particular keystroke combinations listed in the preceding paragraph and upon finding them invokes the proper set of functions to complete the requested Clipboard operation (see Listing 9.20). Note that in addition to copying the data to the Clipboard, the Cut operation clears the control's data.
Listing 9.20 ATLCONTROLWIN.CPP--OnKeyDown Implementation LRESULT CATLControlWin::OnKeyDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { UINT nChar = wParam; UINT nRepCnt = LOWORD(lParam); UINT nFlags = HIWORD(lParam); // find out if the shift key is being held down short sShift = ::GetKeyState(VK_SHIFT); // find out if the control key is being held down short sControl = ::GetKeyState(VK_CONTROL); switch(nChar) { // COPY case 0x43: // `C' case 0x63: // `c' case VK_INSERT: // if the control key is being held down if(sControl & 0x8000) { // copy the data to the clipboard this->CopyDataToClipboard();
// we don't need to pass this key to the base implementation bHandled = TRUE; } break; // CUT case 0x58: // `X' case 0x78: // `x' case VK_DELETE: // if this is a shift delete OR CTRL-X/x if((nChar == VK_DELETE && (sShift & 0x8000)) || ((nChar == 0x58 || nChar == 0x78) && (sControl & 0x8000))) { this->CopyDataToClipboard(); // clear the string since this is a CUT operation delete [] m_lptstrCaption; // NULL terminate the string reference m_lptstrCaption = new TCHAR[1]; m_lptstrCaption[0] = `\0'; // fire the global change event this->FireChange(); // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); cfFormat == CF_TEXT && lpFormatEtc->tymed & TYMED_HGLOBAL) { // get a copy of the current stgmedium this->CopyStgMedium(lpStgMedium, &sTextStgMedium, CF_TEXT); return S_OK; } else return IDataObjectImpl::GetData(lpFormatEtc, lpStgMedium); } STDMETHODIMP CATLControlWin::EnumFormatEtc(DWORD dwDirection, LPENUMFORMATETC* ppenumFormatEtc) { // we support "get" operations if(dwDirection == DATADIR_GET) { // make the assignment *ppenumFormatEtc = (IEnumFORMATETC *) this; // increment the reference count
(*ppenumFormatEtc)->AddRef(); // return success return S_OK; } return IDataObjectImpl::EnumFormatEtc( dwDirection, ppenumFormatEtc); } . . . The IEnumFORMATETC implementation is also simple (see Listing 9.28). The one thing to note is that there is no default ATL implementation, so it is not necessary to defer to the base class implementation in the case where you do not handle the function. Next is used to retrieve the next element in the enumeration; in this case, you need be concerned with only one element, CF_TEXT. After filling in the FORMATETC structure with the appropriate data, you must increment the counter and exit the function. The Skip method increments the counter and exits the function, and Reset sets the counter back to 0.
Listing 9.28 ATLCONTROLWIN.CPP--IEnumFORMATETC Interface Implementation STDMETHODIMP CATLControlWin::Next(ULONG celt, FORMATETC_RPC_FAR * rgelt, ULONG RPC_FAR * pceltFetched) { // if we are at the beginning of the enumeration if(ulFORMATETCElement == 0 && celt > 0) { // copy all of the members rgelt->cfFormat = CF_TEXT; rgelt->ptd = NULL; rgelt->dwAspect = 0; rgelt->lindex = -1; rgelt->tymed = TYMED_HGLOBAL; // if the caller wants to know how many we copied if(pceltFetched) *pceltFetched = 1; // increment the counter ulFORMATETCElement++; // return success return S_OK; } else // return failure return S_FALSE; } STDMETHODIMP CATLControlWin::Skip(ULONG celt) { // move the counter by the number of elements supplied ulFORMATETCElement += celt; // return success return S_OK;
} STDMETHODIMP CATLControlWin::Reset(void) { // reset to the beginning of the enumerator ulFORMATETCElement = 0; // return success return S_OK; } STDMETHODIMP CATLControlWin::Clone( IEnumFORMATETC_RPC_FAR *__RPC_FAR * /*ppenum*/) { return E_NOTIMPL; } Now that you know how to copy data to the Clipboard, you look at how to get data from the Clipboard. Enabling a Control as a Clipboard Target The opposite of being a Clipboard source is being a Clipboard target. First you need to update the CATLControlWin class declaration to include two new helper functions (see Listing 9.29).
Listing 9.29 ATLCONTROLWIN.H--Clipboard Target Support Helper Function Prototypes . . . void CopyDataToClipboard(void); void PrepareDataForTransfer(void); void GetDataFromClipboard(void); void GetDataFromTransfer(IDataObject * ipDataObj); void CopyStgMedium(LPSTGMEDIUM lpTargetStgMedium, LPSTGMEDIUM lpSourceStgMedium, CLIPFORMAT cfSourceFormat); . . . Getting data from the Clipboard is almost as easy as putting the data on the Clipboard in the first place. The first method is GetDataFromClipboard, which, as the name implies, gets the data from the Clipboard and transfers it to the control. The function first checks the Clipboard to see whether the control already owns the Clipboard. If the control does own the Clipboard, it refreshes the control's data with the data that is stored in the STGMEDIUM structure. The implementation is written this way because the data stored in the control may have changed since it was pasted to the Clipboard in the first place. If you don't already own the Clipboard, you get the IDataObject reference of the object that does, and you pass the reference on to the GetDataFromTransfer function (see Listing 9.30).
Listing 9.30 MFCCONTROLWINCTL.CPP--GetDataFromClipboard Implementation void CATLControlWin::GetDataFromClipboard(void) { // get an IDataObject pointer IDataObject * ipClipboardDataObj = NULL;
// do we have an IDataObject on the clipboard? if(::OleIsCurrentClipboard((IDataObject *) this) == S_OK) { // get the global data for this format and lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sTextStgMedium.hGlobal); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpTempBuffer); // unlock the memory ::GlobalUnlock(sTextStgMedium.hGlobal); return; } else if(::OleGetClipboard(&ipClipboardDataObj) == S_OK) { // transfer the data to the control this->GetDataFromTransfer(ipClipboardDataObj); // release the IDataObject ipClipboardDataObj->Release(); } } GetDataFromTransfer requests the IEnumFORMATETC interface from the IDataObject and cycles through all of the supported formats looking for one that matches CF_TEXT (see Listing 9.31). Upon finding the appropriate format, it requests the data from the IDataObject supplying a FORMATETC and a STGMEDIUM structure. The data is transferred to the control, and the STGMEDUIM is released. Finally you release the interface pointers and, if you found a format, force the control to repaint itself reflecting the new state of the control.
Listing 9.31 MFCCONTROLWINCTL.CPP--GetDataFromTransfer Implementation void CATLControlWin::GetDataFromTransfer(IDataObject * ipDataObj) { IEnumFORMATETC * ipenumFormatetc; BOOL bFound = FALSE; // get a FORMATETC enumerator if(ipDataObj->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc) == S_OK) { // reset the enumerator just to be safe ipenumFormatetc->Reset(); FORMATETC etc; // while there are formats to enumerate while(ipenumFormatetc->Next(1, &etc, NULL) == S_OK)
{ // is this a format that we are looking for? if(etc.cfFormat == CF_TEXT && etc.tymed & TYMED_HGLOBAL) { STGMEDIUM sStgMediumData; // get the data from the stgmedium if(ipDataObj->GetData(&etc, &sStgMediumData) == S_OK) { // get the global data for this format // and lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sStgMediumData.hGlobal); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpTempBuffer); // unlock the memory ::GlobalUnlock(sStgMediumData.hGlobal); // release the storage medium ::ReleaseStgMedium(&sStgMediumData); // indicate success bFound = TRUE; } } } // release the enumerator ipenumFormatetc->Release(); } // if we found a format if(bFound == TRUE) // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); GetDataFromClipboard(); // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); GetDataFromClipboard(); // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); FireChange(); // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); GetDataFromTransfer(pDataObject); // return success return S_OK; } As with your MFC (and later BaseCtl), implementations adding Drag and Drop support are straightforward. Now that you have addressed the built-in data transfer formats, you can take a look at the next step, custom data formats.
Custom Clipboard and Drag and Drop Formats A custom data format is one that is understood by the exchanging applications but does not fall into the category of predefined formats. For your implementation, you transfer the text Alignment property along with the Caption. You are not restricted in any way in the types of data that can be transferred in this manner. Adding custom data formats is independent of the mechanism used to initiate the data transfer. Since you have modeled your data transfer methods based on this principle, you need to make only one set of changes to your application to accommodate both custom Clipboard and Drag and Drop operations. The first step is adding the member variables that you will use to implement the custom format (see Listing 9.42). The member variable m_uiCustomFormat will be used to hold the ID number of the registered custom format. The remaining members are used to hold the data and its related formatting information.
Listing 9.42 ATLCONTROLWIN.H--Custom Data Format Member Variables . . . private: FORMATETC sTextFormatEtc; STGMEDIUM sTextStgMedium; // custom format storage variables UINT m_uiCustomFormat; FORMATETC sCustomFormatEtc; STGMEDIUM sCustomStgMedium; }; The next step is to register the custom format and initialize the member variables to valid values, which you do in the constructor (see Listing 9.43). When you register the custom data format, you are actually registering the format in the Windows OS. That way, whenever an application needs to use the format, that application will get the same value as the application that registered the format in the first place. All applications that need to use a
custom format must call this method to retrieve the ID associated with the custom format type.
Listing 9.43 ATLCONTROLWIN.H--Register the Custom Format and Initialize the Member Variables in the Class Constructor . . . // set to the first element ulFORMATETCElement = 0; // clear the storage medium sTextStgMedium.hGlobal = NULL; // register a custom clipboard format m_uiCustomFormat = ::RegisterClipboardFormat("BCFControlCtlCustomFormat"); // clear the storage medium sCustomStgMedium.hGlobal = NULL; } Next you update the PrepareDataForTransfer function to support the custom data format (see Listing 9.44). In addition to the CF_TEXT format, you add the creation of the custom data format, if there is one. You store the new format in the custom storage variables so that you can support the formats on a granular basis. If the application receiving the data understands only the text format, that is all that the application needs to retrieve; but if the application understands both, the application can get both.
Listing 9.44 ATLCONTROLWIN.CPP--PrepareDataForTransfer Update void CATLControlWin::PrepareDataForTransfer(void) { . . . // if we have custom clipboard format support if(m_uiCustomFormat) { // create a global memory object HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sizeof(m_lAlignment)); // lock the memory down LONG * lpTempBuffer = (LONG *) ::GlobalLock(hGlobal); // set our data buffer *lpTempBuffer = m_lAlignment; // unlock the memory ::GlobalUnlock(hGlobal); // copy all of the members sCustomFormatEtc.cfFormat = m_uiCustomFormat; sCustomFormatEtc.ptd = NULL; sCustomFormatEtc.dwAspect = 0; sCustomFormatEtc.lindex = -1; sCustomFormatEtc.tymed = TYMED_HGLOBAL; // if we have already allocated the data if(sCustomStgMedium.hGlobal) // release it ::ReleaseStgMedium(&sCustomStgMedium);
sCustomStgMedium.tymed = TYMED_HGLOBAL; sCustomStgMedium.hGlobal = hGlobal; sCustomStgMedium.pUnkForRelease = NULL; } } Next you update the GetDataFromTransfer method (see Listing 9.45), which you will use to copy the data from a SGTMEDUIM structure to the control. As with the PrepareDataForTransfer method, you take a granular approach and support the basic text transfer independent of the custom format. Note that you change the while loop slightly so that you can look through all of the available formats and stop only when you have looked at all of them. This way, you can get the text format and the custom format independent of each other, thus preventing them from being mutually exclusive.
Listing 9.45 ATLCONTROLWIN.CPP--GetDataFromTransfer Update void CATLControlWin::GetDataFromTransfer(IDataObject * ipDataObj) { IEnumFORMATETC * ipenumFormatetc; BOOL bFound = FALSE; // get a FORMATETC enumerator if(ipDataObj->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc) == S_OK) { // reset the enumerator just to be safe ipenumFormatetc->Reset(); FORMATETC etc; // while there are formats to enumerate while(ipenumFormatetc->Next(1, &etc, NULL) == S_OK) { // is this a format that we are looking for? if(etc.cfFormat == CF_TEXT && etc.tymed & TYMED_HGLOBAL) { STGMEDIUM sStgMediumData; // get the data from the stgmedium if(ipDataObj->GetData(&etc, &sStgMediumData) == S_OK) { // get the global data for this format // and lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sStgMediumData.hGlobal); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpTempBuffer);
// unlock the memory ::GlobalUnlock(sStgMediumData.hGlobal); // release the storage medium ::ReleaseStgMedium(&sStgMediumData); // indicate success bFound = TRUE; } } // is this a format that we are looking for? else if(m_uiCustomFormat && etc.cfFormat == m_uiCustomFormat && etc.tymed & TYMED_HGLOBAL) { STGMEDIUM sStgMediumData; // get the data from the stgmedium if(ipDataObj->GetData(&etc, &sStgMediumData) == S_OK) { // get the global data for this format and lock down the memory LONG * lpTempBuffer = (LONG *) ::GlobalLock(sStgMediumData.hGlobal); // get the data m_lAlignment = *lpTempBuffer; // unlock the memory ::GlobalUnlock(sStgMediumData.hGlobal); // release the storage medium ::ReleaseStgMedium(&sStgMediumData); // indicate success bFound = TRUE; } } } // release the enumerator ipenumFormatetc->Release(); } // if we found a format if(bFound == TRUE) // force the control to repaint itself this->FireViewChange(); // this->InvalidateControl(); 0) { // copy all of the members rgelt->cfFormat = CF_TEXT; rgelt->ptd = NULL; rgelt->dwAspect = 0; rgelt->lindex = -1; rgelt->tymed = TYMED_HGLOBAL; // if the caller wants to know how many we copied if(pceltFetched) *pceltFetched = 1; // increment the counter ulFORMATETCElement++; // return success return S_OK; } else if(m_uiCustomFormat && ulFORMATETCElement == 1 && celt > 0) { // copy all of the members rgelt->cfFormat = m_uiCustomFormat; rgelt->ptd = NULL; rgelt->dwAspect = 0; rgelt->lindex = -1; rgelt->tymed = TYMED_HGLOBAL; // if the caller wants to know how many we copied if(pceltFetched) *pceltFetched = 1; // increment the counter ulFORMATETCElement++; // return success return S_OK; } else // return failure return S_FALSE; } Last you need to update the routine that returns the custom format in the STGMEDIUM structure, IEnumFORMATETC::GetData (see Listing 9.47). You can still use the CopyStgMedium function; the only difference is which internal STGMEDIUM structure is supplied to the function.
Listing 9.47 ATLCONTROLWIN.CPP--IEnumFORMATETC::GetData Update STDMETHODIMP CATLControlWin::GetData(LPFORMATETC lpFormatEtc, LPSTGMEDIUM lpStgMedium) { // if this is a format that we can deal with if(lpFormatEtc->cfFormat == CF_TEXT && lpFormatEtc->tymed & TYMED_HGLOBAL)
{ // get a copy of the current stgmedium this->CopyStgMedium(lpStgMedium, &sTextStgMedium, CF_TEXT); return S_OK; } else if(m_uiCustomFormat && lpFormatEtc->cfFormat == m_uiCustomFormat && lpFormatEtc->tymed & TYMED_HGLOBAL) { // get a copy of the current stgmedium this->CopyStgMedium(lpStgMedium, &sCustomStgMedium, m_uiCustomFormat); return S_OK; } else return IDataObjectImpl::GetData(lpFormatEtc, lpStgMedium); } That's all it takes to support custom formats. By taking a "black box" approach to how you've created the basic data transfer routines, you can support a large amount of functionality, while relying on a relatively small common code base.
Subclassing Existing Windows Controls As with your MFC implementation, ATL allows for the creation of controls that subclass existing Windows controls. At the beginning of this chapter, you created several controls in the application, one of which subclassed a Windows BUTTON control. The CATLControlSubWin class varies little from the CATLControlWin class. All of the differences are contained within the ATLControlSubWin.h header file (see Listing 9.48). ATL relies on the class CContainedWindow to implement its subclassing feature. The constructor of the primary control is where the constructor of the contained class is supplied with the name of the Windows control to use when performing the actual subclassing routine. The ATL Object Wizard also adds a message handler for the WM_CREATE message that instructs the contained window to create itself. The function SetObjectRects is overloaded so that the contained control will size itself correctly in tandem with the container control.
Listing 9.48 ATLCONTROLSUBWIN.H--CATLControlSubWin Class Implementation ///////////////////////////////////////////////////////////////////////////// // CATLControlSubWin class ATL_NO_VTABLE CATLControlSubWin : public CComObjectRootEx, public CComCoClass, public CComControl, public IDispatchImpl, public IProvideClassInfo2Impl, public IPersistStreamInitImpl, public IPersistStorageImpl,
public IQuickActivateImpl, public IOleControlImpl, public IOleObjectImpl, public IOleInPlaceActiveObjectImpl, public IViewObjectExImpl, public IOleInPlaceObjectWindowlessImpl, public IDataObjectImpl, public ISupportErrorInfo, public IConnectionPointContainerImpl, public ISpecifyPropertyPagesImpl { public: CContainedWindow m_ctlButton; CATLControlSubWin() : m_ctlButton(_T("Button"), this, 1) { m_bWindowOnly = TRUE; } DECLARE_REGISTRY_RESOURCEID(IDR_ATLCONTROLSUBWIN) BEGIN_COM_MAP(CATLControlSubWin) COM_INTERFACE_ENTRY(IATLControlSubWin) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY_IMPL(IViewObjectEx) COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject2, IViewObjectEx) COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject, IViewObjectEx) COM_INTERFACE_ENTRY_IMPL(IOleInPlaceObjectWindowless) COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleInPlaceObject, IOleInPlaceObjectWindowless) COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleWindow, IOleInPlaceObjectWindowless) COM_INTERFACE_ENTRY_IMPL(IOleInPlaceActiveObject) COM_INTERFACE_ENTRY_IMPL(IOleControl) COM_INTERFACE_ENTRY_IMPL(IOleObject) COM_INTERFACE_ENTRY_IMPL(IQuickActivate) COM_INTERFACE_ENTRY_IMPL(IPersistStorage) COM_INTERFACE_ENTRY_IMPL(IPersistStreamInit) COM_INTERFACE_ENTRY_IMPL(ISpecifyPropertyPages) COM_INTERFACE_ENTRY_IMPL(IDataObject) COM_INTERFACE_ENTRY(IProvideClassInfo) COM_INTERFACE_ENTRY(IProvideClassInfo2) COM_INTERFACE_ENTRY(ISupportErrorInfo) COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer) END_COM_MAP() BEGIN_PROPERTY_MAP(CATLControlSubWin) // PROP_ENTRY("Description", dispid, clsid) PROP_PAGE(CLSID_CColorPropPage) END_PROPERTY_MAP() BEGIN_CONNECTION_POINT_MAP(CATLControlSubWin) END_CONNECTION_POINT_MAP() BEGIN_MSG_MAP(CATLControlSubWin) MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode) MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus) MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus) MESSAGE_HANDLER(WM_CREATE, OnCreate) ALT_MSG_MAP(1) // Replace this with message map entries for subclassed Button END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { RECT rc; GetWindowRect(&rc); rc.right -= rc.left; rc.bottom -= rc.top; rc.top = rc.left = 0; m_ctlButton.Create(m_hWnd, rc); return 0; } STDMETHOD(SetObjectRects)(LPCRECT prcPos,LPCRECT prcClip) { IOleInPlaceObjectWindowlessImpl::SetObjectRects( prcPos, prcClip); int cx, cy; cx = prcPos->right - prcPos->left; cy = prcPos->bottom - prcPos->top; ::SetWindowPos(m_ctlButton.m_hWnd, NULL, 0, 0, cx, cy, SWP_NOZORDER | SWP_NOACTIVATE); return S_OK; } // ISupportsErrorInfo STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // IViewObjectEx STDMETHOD(GetViewStatus)(DWORD* pdwStatus) { ATLTRACE(_T("IViewObjectExImpl::GetViewStatus\n")); *pdwStatus = VIEWSTATUS_SOLIDBKGND | VIEWSTATUS_OPAQUE; return S_OK; } // IATLControlSubWin public: HRESULT OnDraw(ATL_DRAWINFO& di); }; The ATL Sample Index provides access to several sample applications that subclass Windows controls. Because subclassing is straightforward and easy to implement, there is not much to the samples. Subclassing is an easy way to add specialized controls to your application. Providing list boxes or buttons that perform unique actions relative to your implementation can save you a lot of development time, especially if there is use for the controls in more than one instance. Subclassing involves very little work and provides a lot of benefit.
Dual-Interface Controls
ATL control implementations, by default, support dual-interface, so no extra work is needed. As we stated in previous chapters, however, currently no control containers can or will use dual-interfaces within controls.
NOTE: Unclipped device context is an MFC-specific optimization and is not implemented in ATL.
Other ActiveX Features As with your MFC implementation, ATL allows you to take advantage of some of the available OC 96 or ActiveX features. Chapter 6 contains a detailed explanation of each feature, so we don't go into them here. We do, however, look into the aspects of their specific implementation and how they relate to ATL.
Windowless Activation Windowless activation is supported through the IOleInPlaceObjectWindowless interface and is implemented in the container. If the container doesn't support windowless activation, the control must be able to create a window for itself. Windowless activation is a request not a guarantee. ATL support of windowless controls is easy to implement. The CComControl class contains a member variable m_bWindowOnly, which, if set to TRUE, instructs the control to use its windowless feature if the control supports it. If set to FALSE, the control will always create a window handle. The class CATLControlNoWin, which is the class of the control you created at the beginning of the chapter to support windowless activation, sets the m_bWindowOnly member to TRUE within its constructor. A number of member variables and functions are related to windowless control creation; so you need to review the ATL documentation thoroughly.
Flicker-Free Activation Flicker-free activation is based on the IOleInPlaceSiteEx interface. The ATL framework automatically attempts to find this interface, so flicker-free activation requires no implementation on the part of the developer.
Mouse Pointer Notifications When Inactive Mouse pointer notifications when inactive are provided through the COM interface IPointerInactive. To support the interface in your control, you must add the ATL class IPointerInactiveImpl to your class declaration and override the ATL implementations of the GetActivationPolicy, OnInactiveMouseMove, and OnInactiveSetCursor.
Optimized Drawing Code Optimized drawing is handled much as in MFC, as you saw in the optimized drawing section earlier in this chapter. A parameter of the OnDraw method indicates whether the control can draw using the optimized techniques you first saw in your MFC implementation. In addition, the ATL implementation allows for aspect or optimized drawing. Drawing with aspects is beyond the scope of this book. If you want to implement this feature, please see the OC 96 specification included in the ActiveX SDK.
Loads Properties Asynchronously To support asynchronous properties, a control must support the stock property ReadyState and the stock event ReadyStateChange. The control is responsible for updating the property and notifying the container when it has changed. Other than the OnData function, which is already provided by the ATL framework, the implementation must be performed manually by the developer of the control, as you saw in the asynchronous property section earlier in this chapter.
From Here... This chapter focused on expanding the basic control implementation that you created in Chapter 8. The advanced features and functionality that you learned, such as asynchronous properties, Drag and Drop support, and many others, will allow you to distinguish your control implementation as being a truly professional implementation. With the introduction of version 2.1, ATL has come full circle and provides you with a complete and robust framework for developing ActiveX controls and ActiveX components in general. The amount of support being put into this product both by the industry and Microsoft should tell you that ATL is the way to do ActiveX development in the future. Chapters 10 and 11 examine in detail how to create a similar control implementation using BaseCtl.
Chapter 10 Using BaseCtl to Create a Basic ActiveX Control ●
Using BaseCtl to Create a Basic ActiveX Control ❍ Creating a Basic Control Project ■ Listing 10.1 BCFCONTROL.CPP--Include New Control Header Files and Control Declarations ■ Listing 10.2 GUIDS.H--Combined Guids.h ■ Listing 10.3 BCFCONTROL.ODL--Combined Control ODL Files ■ Listing 10.4 RESOURCE.H--Combined Resource.h Files ■ Listing 10.5 LOCALOBJ.H--Combined LocalObj.h File ■ Listing 10.6 BCFCONTROLCTL.H--Update the Bitmap Source ❍ Control Registration ■ Listing 10.7 BCFCONTROLCTL.H--DEFINE_CONTROLOBJECT Structure ❍ Creating Methods ■ Listing 10.8 DISPIDS.H--Dispid for CaptionMethod ■ Listing 10.9 BCFCONTROLINTERFACES.H--Interface File Created from the ODL File ■ Listing 10.10 BCFCONTROLCTL.H--Updated Control Class Header File ■ Listing 10.11 ALIGNMENTENUMS.H--Alignment Styles Enumeration ■ Listing 10.12 BCFCONTROLCTL.CPP--Member Variable Initialization ■ Listing 10.13 BCFCONTROLCTL.CPP--CaptionMethod Implementation ❍ Creating Properties ■ Creating Normal User Defined Properties ■ Listing 10.14 BCFCONTROL.ODL--Normal User Defined Property ODL Declaration ■ Listing 10.15 BCFCONTROLCTL.H--Property Function Prototypes ■ Listing 10.16 BCFCONTROLCTL.CPP--Property Function Implementation ■ Creating Parameterized User Defined Properties ■ Listing 10.17 DISPIDS.H--Dispid for CaptionProp Property ■ Listing 10.18 BCFCONTROL.ODL--ODL Entry for Caption Property ■ Listing 10.19 BCFCONTROLCTL.H--CaptionProp Function Prototypes ■ Listing 10.20 BCFCONTROLCTL.CPP--CaptionProp Property Implementation ■ Creating Stock Properties ■ Listing 10.21 BCFCONTROL.ODL--BackColor Stock Property Support ■ Listing 10.22 BCFCONTROLCTL.H--BackColor Function Prototype ■ Listing 10.23 BCFCONTROLCTL.H--BackColor Member Variable ■ Listing 10.24 BCFCONTROLCTL.CPP--Member Initialization ■ Listing 10.25 BCFCONTROLCTL.CPP--BackColor Property Implementation ■ Using Ambient Properties ■ Creating Property Sheets ■ Listing 10.26 BCFCONTROLPPG.CPP--Property Page Implementation ❍ Adding Events ■ Listing 10.27 DISPIDS.H--Change Event Dispid ■ Listing 10.28 BCFCONTROL.ODL--Event ODL Entry
Listing 10.29 BCFCONTROLCTL.H--Event Handling Structures--HeaderFile ■ Listing 10.30 BCFCONTROLCTL.CPP--Event Structures Implementation--Source File ■ Listing 10.31 BCFCONTROLCTL.H--FireChange Event--Header File ■ Listing 10.32 BCFCONTROLCTL.CPP--FireChange Event Implementation Persistence ■ Text Persistence ■ Listing 10.33 BCFCONTROLCTL.CPP--LoadTextState Implementation ■ Listing 10.34 BCFCONTROLCTL.CPP--SaveTextState Implementation ■ Binary Persistence ■ Listing 10.35 BCFCONTROLCTL.CPP--SaveBinaryState Implementation ■ Listing 10.36 BCFCONTROLCTL.CPP--LoadBinaryState Implementation Drawing the Control ■ Standard Drawing ■ Listing 10.37 BCFCONTROLCTL.H--Drawing Implementation Member Variables and Functions ■ Listing 10.38 BCFCONTROLCTL.CPP--Member Initialization ■ Listing 10.39 BCFCONTROLCTL.CPP--Drawing Helper Functions ■ Listing 10.40 BCFCONTROLCTL.CPP--OnDraw Implementation From Here... ■
❍
❍
❍
Using BaseCtl to Create a Basic ActiveX Control ●
●
●
●
●
Registration The BaseCtl framework supports control registration and unregistra-tion through a set of constants and structures that define the basic control framework. Adding methods and properties Like ATL, BaseCtl methods are first implemented in the type library and then in the actual control. Like methods, properties are a way of exposing information about a control implementation to the control's user. Adding events Implementing events in BaseCtl is far easier than in ATL and just as easy to maintain as MFC. Persistence Persistence of data in BaseCtl is at its most basic and differs significantly from that of both MFC and ATL. Drawing the control Drawing the User Interface can make or break a control implementation. Your implementation relies on the same basic drawing routines as the MFC and ATL implementations and requires almost no modification.
The BaseCtl framework was originally created by Marc Wandschneider, a member of the Visual Basic (VB) team, to address the need for small, fast OCXs that could be used within VB without adversely affecting VB's performance. The original implementation was referred to lovingly as the MarcWan framework in honor of its
primary author. Along with the desire for a compact control framework that could be used to create ActiveX controls came the mandate to remove the framework's dependence on MFC, which in our mind is both the BaseCtl's strength and weakness. Removing the BaseCtl's dependence on MFC solved several key problems: code overhead and control load time performance. Since the BaseCtl framework is lean, the controls tend to execute faster, and because the MFC DLLs are not required, the amount of time it takes to load the control into memory is effectively reduced. However, writing a control from scratch using the BaseCtl takes significantly more time since you do not have AppWizards and ClassWizards at your disposal to speed up your implementation. The lack of general class support, for example drawing classes and storage classes, can also increase your development effort. With the coming of ActiveX, the need for small, fast OCXs suddenly became an industry concern. Microsoft's answer to that demand was to publish the BaseCtl as an alternative method (as opposed to MFC) for developing controls. Several versions of the BaseCtl framework are floating around. The basic version, which ships with the ActiveX SDK, consists of a number of source files and several samples. A more thorough version consisting of more samples and even an AppWizard written in VB has been available to the members of the Visual Basic 5 (VB 5) beta testing group. The BaseCtl Framework is intended merely as a sample application and does not have the same support and backing of Microsoft as do its other development products. It was an immediate solution to an immediate problem. The BaseCtl framework has one very distinct disadvantage in that Microsoft considers it a sample application only and does not support it directly as a product. A growing number of developers on the Internet are using BaseCtl, so you should be able to find support easily if you run into a problem. This chapter focuses on creating ActiveX controls using the BaseCtl sample that ships with the ActiveX SDK. Be warned, though, that this kind of control development is not for the timid. Be prepared to roll your sleeves up and get dirty.
Creating a Basic Control Project The version of the BaseCtl sample that ships with the ActiveX SDK does not have an AppWizard for creating a basic control like its MFC counterpart. Also, the documentation is minimal and not much help. To speed up your development, we've included a sample project called BCFBasicControl that is based on the original BaseCtl sample files. Using this sample control, you can create a new project by copying the files and changing the names of the files and classes. First in Table 10.1 examine the files that are needed for creating a basic control with the BaseCtl framework. Table 10.1 BaseCtl Basic Project Files File name
Description
BCFBasicControl.dsw
VC++ build file.
BCFBasicControl.dsp
VC++ build file.
BCFBasicControl.opt
VC++ build file.
BCFBasicControl.cpp
Main control application file.
BCFBasicControl.def
Application definition file.
BCFBasicControl.odl
Object Definition Language file for describing the and interfaces contained in the control.
BCFBasicControl.rc
Application resource file.
BCFBasicControlCtl.bmp Basic control bitmap that appears in the tool browser. BCFBasicControlCtl.cpp
Primary control source file.
BCFBasicControlCtl.h
Primary control header file.
BCFBasicControlPPG.cpp Property page source file. BCFBasicControlPPG.h
Property page header file.
Dispids.h
Header file that contains all of the method, property, event dispids. Add all dispids to this file.
Dwinvers.h
Version information header file. Change this file to the version information specific to the control.
Guids.cpp
Source file for the GUIDs defined in the application.
Guids.h
Header file for the GUID defined in the application.
LocalObj.h
Header file for the OBJECT_TYPE constants that are used in the g_ObjectInfo table to identify the controls and property pages contained in the application.
Resource.h
Header file that contains all of the resource ID constants.
project_nameInterfaces.H This file is not created until the project is compiled. The mktylib (or midl) compiler automatically generates this file from the ODL file. This file contains the C++style declarations for the interfaces of the ActiveX component. Do not modify this file directly; it will be recreated every time the ODL file is compiled.
In this chapter, you will create three controls: a standard control, a windowless control, and a subclassed control. All three will be combined into a single control module to give you a feel for how it is done. None of the sample controls is meant to be a fully functional control. They are used only to give you an understanding of how to implement specific features and functionality with a minimum of effort. Again, since you do not have an application wizard at your disposal, you must create your project by hand. To create a new BaseCtl project, you need to perform the following steps: ●
●
●
●
●
Create a directory for the new project. In this case, call the new project directory BCFControl. Copy all of the files from the BCFBasicControl sample directory to the new directory. Change the names of the files from BCFBasicControl to your project name. In this case, change the names to BCFControl. Using an application, such as Visual C++, that is capable of doing text replacement, change all of the BCFBasicControl entries to the name of your control. In this case, change the name BCFBasicControl to BCFControl and BCFBASICCONTROL to BCFCONTROL, respectively. Remember to change every file that was copied from the basic project and perform the replacement on a case-sensitive basis. Generate new UUIDs with GUIDGEN.EXE. See Chapter 2 for more information about GUIDGEN.EXE and how it is used. Replace the four UUIs in the ODL file and the one property page UUID in the Guids.h file. Modify the Dwinvers.h file to reflect the version and company information that is appropriate for your project.
After you modify all of the files, you are ready to use your new project. Open the Visual C++ development environment, and from the File menu, select the Open Workspace menu item. In the Open Workspace dialog, change to the directory of your newly created project (\Que\ActiveX\BCFControl), and open the BCFControl.dsw
file (see fig. 10.1). FIG. 10.1 Open the new project with the Open Workspace dialog. Unlike with the MFC and ATL projects, to support more than one control within the application, you must add all of the code and files by hand since you do not have an AppWizard. The simplest way to create additional controls is to repeat the steps described earlier and copy the appropriate code and files into your base project. You will create two additional controls named BCFControlNoWinControl, which is a windowless control, and BCFControlSubWinControl, which will subclass a BUTTON window. After creating the two new projects, as described in the preceding paragraph, copy the following files to the BCFControl directory. ● ● ● ● ● ● ● ● ● ●
BCFControlNoWinCtl.bmp BCFControlNoWinCtl.cpp BCFControlNoWinCtl.h BCFControlNoWinPPG.cpp BCFControlNoWinPPG.h BCFControlSubWinCtl.bmp BCFControlSubWinCtl.cpp BCFControlSubWinCtl.h BCFControlSubWinPPG.cpp BCFControlSubWinPPG.h
Ensure that the BCFControl project is open within the VC++ IDE, and from the Project menu, select the Add to Project menu item and the Files submenu. In the Insert Files into Project dialog, select the files BCFControlNoWinCtl.Cpp, BCFControlNoWinPPG.Cpp, BCFControlSubWinCtl.Cpp, and BCFControlSubWinPPG.Cpp, and click the OK button (see fig. 10.2). FIG. 10.2 Use the Insert Files into Project dialog to add the files of the BCFControlNoWin and BCFControlSubWin control to the base project. After you add all of the files that you need to your project, you still have to do some cut and paste operations from the remaining files to complete the integration of the three controls. You need to include the header files from your new control projects to the main application file (see Listing 10.1). The BaseCtl framework supports a globally declared array for describing all of the OLE components included within the application. The OBJECTINFO array, as it is called, should contain an entry for each control, property page, and automation server you want to declare within your module. You need to add your additional control and property page declarations to the OBJECTINFO array.
Listing 10.1 BCFCONTROL.CPP--Include New Control Header Files and Control Declarations //=-------------------------------------------------------------------------= // BCFControl.Cpp //=---------------------------------------------------------
-----------------= ... #include "BCFControlCtl.H" #include "BCFControlPPG.H" #include "BCFControlNoWinCtl.H" #include "BCFControlNoWinPPG.H" #include "BCFControlSubWinCtl.H" #include "BCFControlSubWinPPG.H" ... //=-------------------------------------------------------------------------= // This Table describes all the automatible objects in your automation server. // See AutomationObject.H for a description of what goes in this structure // and what it's used for. // OBJECTINFO g_ObjectInfo[] = { CONTROLOBJECT(BCFControl), PROPERTYPAGE(BCFControlGeneral), CONTROLOBJECT(BCFControlNoWin), PROPERTYPAGE(BCFControlNoWinGeneral), CONTROLOBJECT(BCFControlSubWin), PROPERTYPAGE(BCFControlSubWinGeneral), EMPTYOBJECT }; ... The ODL compiler will generate a C++ header file for accessing the interfaces declared in the application. Since you are combining all three controls into a single project, you will have a single interface file to deal with. The two additional control implementations must be changed to reflect the new file. In the BCFControlNoWinCtl.h header file, you need to change the include file #include "BCFControlNoWinInterfaces.H" to #include "BCFControlInterfaces.H" The same must be done for the BCFControlSubWinCtl.h header file. Each of the individual projects contains a Guids.h file. You combine all three into a single file (see Listing 10.2).
Listing 10.2 GUIDS.H--Combined Guids.h #ifndef _GUIDS_H_ // for each property page this server will have, put the guid definition for it // here so that it gets defined ... // DEFINE_GUID(CLSID_BCFControlGeneralPage, 0x317512F4, 0x3E75, 0x11d0,
0xBE, 0xBE, 0x00, 0x40, 0x05, 0x38, 0x97, 0x7D); DEFINE_GUID(CLSID_BCFControlNoWinGeneralPage, 0xcf395064, 0x3fb6, 0x11d0, 0xbe, 0xc1, 0x00, 0x40, 0x05, 0x38, 0x97, 0x7d); DEFINE_GUID(CLSID_BCFControlSubWinGeneralPage, 0x02456be4, 0x3fb7, 0x11d0, 0xbe, 0xc1, 0x00, 0x40, 0x05, 0x38, 0x97, 0x7d); #define _GUIDS_H_ #endif // _GUIDS_H_ Next you need to combine all of the ODL files into a single file. While it is possible for an application to contain more than one type library resource, for simplicity, you will have one. Copy only the interface entries from each of the ODL files, and insert them into the BCFControl.odl file (see Listing 10.3).
Listing 10.3 BCFCONTROL.ODL--Combined Control ODL Files //=-------------------------------------------------------------------------= // BCFControl.ODL //=-------------------------------------------------------------------------= // Copyright 1995 Microsoft Corporation. All Rights Reserved. // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. //=-------------------------------------------------------------------------= // // ODL file for the control(s) and automation object(s) in this inproc server // #include #include "dispids.h" // can't include oaidl.h, so this will have to do // #define DISPID_NEWENUM -4 //=-------------------------------------------------------------------------= // the libid for this type libray // [ uuid(317512F0-3E75-11d0-BEBE-00400538977D), helpstring("BCFControl Control Library"), lcid(0x0000), version(1.0) ] library BCFControlObjects { // standard imports // importlib("STDOLE32.TLB"); importlib(STDTYPE_TLB); // primary dispatch interface for CBCFControl control //
[ uuid(317512F1-3E75-11d0-BEBE-00400538977D), helpstring("BCFControl Control"), hidden, dual, odl ] interface IBCFControl : IDispatch { // properties // // methods // [id(DISPID_ABOUTBOX)] void AboutBox(void); }; // event interface for CBCFControl controls ... // [ uuid(317512F2-3E75-11d0-BEBE-00400538977D), helpstring("Event interface for BCFControl control"), hidden ] dispinterface DBCFControlEvents { properties: methods: }; // coclass for CBCFControl controls // [ uuid(317512F3-3E75-11d0-BEBE-00400538977D), helpstring("BCFControl control") ] coclass BCFControl { [default] interface IBCFControl; [default, source] dispinterface DBCFControlEvents; }; // primary dispatch interface for CBCFControlNoWin control // [ uuid(cf395061-3fb6-11d0-bec1-00400538977d), helpstring("BCFControlNoWin Control"), hidden, dual, odl ] interface IBCFControlNoWin : IDispatch { // properties // // methods // [id(DISPID_ABOUTBOX)] void AboutBox(void);
}; // event interface for CBCFControlNoWin controls ... // [ uuid(cf395062-3fb6-11d0-bec1-00400538977d), helpstring("Event interface for BCFControlNoWin control"), hidden ] dispinterface DBCFControlNoWinEvents { properties: methods: }; // coclass for CBCFControlNoWin controls // [ uuid(cf395063-3fb6-11d0-bec1-00400538977d), helpstring("BCFControlNoWin control") ] coclass BCFControlNoWin { [default] interface IBCFControlNoWin; [default, source] dispinterface DBCFControlNoWinEvents; }; // primary dispatch interface for CBCFControlSubWin control // [ uuid(02456be1-3fb7-11d0-bec1-00400538977d), helpstring("BCFControlSubWin Control"), hidden, dual, odl ] interface IBCFControlSubWin : IDispatch { // properties // // methods // [id(DISPID_ABOUTBOX)] void AboutBox(void); }; // event interface for CBCFControlSubWin controls ... // [ uuid(02456be2-3fb7-11d0-bec1-00400538977d), helpstring("Event interface for BCFControlSubWin control"), hidden ] dispinterface DBCFControlSubWinEvents { properties: methods: }; // coclass for CBCFControlSubWin controls //
[ uuid(02456be3-3fb7-11d0-bec1-00400538977d), helpstring("BCFControlSubWin control") ] coclass BCFControlSubWin { [default] interface IBCFControlSubWin; [default, source] dispinterface DBCFControlSubWinEvents; }; };
CAUTION: When combining several OCXs, you must delete any .tlb files that may have already been created to update the dependencies of the project. Occasionally, Visual C++ will compile more that one type library into different directories and use the incorrect version when compiling the resources for the control. In the case where you have new information that is added to the ODL file, the result will be that the type library has not appeared to have changed, which can be a major problem for applications, such as VB, that depend on the type library for information about the component being used.
Open the BCFControlNoWin.rc and BCFControlSubWin.rc resource files. Copy the property page dialogs and the string and bitmap resources to the BCFControl resource file. Rename the bitmaps to RESID_TOOLBOX_BITMAP1, RESID_TOOLBOX_BITMAP2, and RESID_TOOLBOX_BITMAP3, respectively.
NOTE: If you experience problems opening the resource files because of dependencies on the type library, you can either compile the ODL files into the type library or open the resource files in text mode and edit the files manually.
After combining the resources, you need to combine your Resource.h files as well (see Listing 10.4). Make sure that the constant values match their corresponding resources.
Listing 10.4 RESOURCE.H--Combined Resource.h Files //=-------------------------------------------------------------------------= // Resource.H //=-------------------------------------------------------------------------= // Copyright 1995 Microsoft Corporation. All Rights Reserved. // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. //=-------------------------------------------------------------------------=
// // resource IDs. // #ifndef _RESOURCE_H_ #define RESID_TOOLBOX_BITMAP1 1 #define RESID_TOOLBOX_BITMAP2 2 #define RESID_TOOLBOX_BITMAP3 3 //=-------------------------------------------------------------------------= // Strings // // this must be defined for any server that has propety pages. it must be one // thousand. // #define IDS_PROPERTIES 1000 // this is defined for all inproc servers that use satellite localization. it // must be 1001 // #define IDS_SERVERBASENAME 1001 #define IDS_BCFCONTROL_GENERALPAGETITLE 2003 #define IDS_BCFCONTROL_GENERALDOCSTRING 2004 #define IDS_BCFCONTROLNOWIN_GENERALPAGETITLE 2005 #define IDS_BCFCONTROLNOWIN_GENERALDOCSTRING 2006 #define IDS_BCFCONTROLSUBWIN_GENERALPAGETITLE 2007 #define IDS_BCFCONTROLSUBWIN_GENERALDOCSTRING 2008 #define IDS_BCFCONTROL_ABOUTBOXVERB 2009 #define IDS_BCFCONTROLNOWIN_ABOUTBOXVERB 2010 #define IDS_BCFCONTROLSUBWIN_ABOUTBOXVERB 2011 //=-------------------------------------------------------------------------= // Dialog Stuff // #define IDD_PROPPAGE_BCFCONTROLGENERAL 2000 #define IDD_PROPPAGE_BCFCONTROLNOWINGENERAL 2001 #define IDD_PROPPAGE_BCFCONTROLSUBWINGENERAL 2002 #define _RESOURCE_H_ #endif // _RESOURCE_H_ Combine all of the LocalObj.h files (see Listing 10.5). Be sure to renumber the constants so that they are not the same since they are used to identify each control and property page in your application.
Listing 10.5 LOCALOBJ.H--Combined LocalObj.h File //=-------------------------------------------------------------------------= // LocalObjects.H //=-------------------------------------------------------------------------= ... // **** ADD ALL NEW OBJECTS TO THIS LIST **** //
#define OBJECT_TYPE_CTLBCFCONTROL 0 #define OBJECT_TYPE_PPGBCFCONTROLGENERAL 1 #define OBJECT_TYPE_CTLBCFCONTROLNOWIN 2 #define OBJECT_TYPE_PPGBCFCONTROLNOWINGENERAL 3 #define OBJECT_TYPE_CTLBCFCONTROLSUBWIN 4 #define OBJECT_TYPE_PPGBCFCONTROLSUBWINGENERAL 5 #define _LOCALOBJECTS_H_ #endif // _LOCALOBJECTS_H_ Last you update the bitmap resource constant for each of your control header files. Listing 10.6 shows the change that needs to be made to each of the control header files.
Listing 10.6 BCFCONTROLCTL.H--Update the Bitmap Source extern const GUID *rgBCFControlPropPages []; DEFINE_CONTROLOBJECT(BCFControl, &CLSID_BCFControl, "BCFControlCtl", CBCFControlControl::Create, 1, &IID_IBCFControl, "BCFControl.HLP", &DIID_DBCFControlEvents, OLEMISC_SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_RECOMPOSEONRESIZE | OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT, 0, // no IPointerInactive policy by default RESID_TOOLBOX_BITMAP1, "BCFControlWndClass", 1, rgBCFControlPropPages, 0, NULL); Be sure to make the same change to the BCFControlNoWinCtl.h and BCFControlSubWinCtl.h header files using the ID's RESID_TOOLBOX_BITMAP2 and RESID_TOOLBOX_BITMAP3, respectively. All of the basic source files are now added to the control project. The next step in any control project is to ensure that the project contains registration support. Without registration, the control cannot be used by any application.
Control Registration Control registration support is handled completely by the BaseCtl framework and is hidden from the developer. Some of the registration information is part of the DEFINE_CONTROLOBJECT structure (see Listing 10.7), which you looked at earlier in this chapter. See the BaseCtl documentation about the specific information that can be changed.
Listing 10.7 BCFCONTROLCTL.H--DEFINE_CONTROLOBJECT Structure // TODO: if you have an array of verbs, then add an extern here with the name
// of it, so that you can include it in the DEFINE_CONTROLOBJECT. // ie. extern VERBINFO m_BCFControlCustomVerbs []; // extern const GUID *rgBCFControlPropPages []; DEFINE_CONTROLOBJECT(BCFControl, &CLSID_BCFControl, "BCFControlCtl", CBCFControlControl::Create, 1, &IID_IBCFControl, "BCFControl.HLP", &DIID_DBCFControlEvents, OLEMISC_SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_RECOMPOSEONRESIZE | OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT, 0, // no IPointerInactive policy by default RESID_TOOLBOX_BITMAP1, "BCFControlWndClass", 1, rgBCFControlPropPages, 0, NULL);
NOTE: The BaseCtl basic project does not by default contain support for registering the control automatically when the project is compiled. You must ensure that the control is properly registered before using it, especially if you recently switched between debug and release versions.
You can now compile and register the control you've created, but it won't be of much use because it doesn't contain any methods, properties, or events, which are the basis of every ActiveX control.
Creating Methods Now that you've successfully created your ActiveX control project, you can add a method, which is one of the basic aspects of component development. For the purposes of the sample control, you add a method called CaptionMethod. The method accepts two parameters, the second being optional. The first parameter is a string that the control displays within its client area, and the second, optional parameter is the alignment of the caption within the client area, either left, right, or center. When creating methods, properties, and events for your control, you always start with the ODL file. When the ODL file is compiled, a C++ header file is created that defines all of the interfaces and methods of the control. From the header file, you cut and paste the method, property, and event prototypes into your control implementation. The one thing about your BaseCtl (and ATL) implementation that is different from your MFC implementation is that your control supports dual-interfaces by default. All methods, properties, and events must be written using dual-interface syntax rules, which you get into a little later in this chapter. First you need to add an entry to the Dispids.h file. Dispids are the constant values used by the IDispatch
routines to locate the correct method or property being invoked through the IDispatch interface. Dispids can be any unique number including negative numbers. Be careful when using negative values, however, since the systemdefined dispids are all negative. You are better off staying with only positive values. Listing 10.8 shows the dispid entry that you added for your CaptionMethod.
Listing 10.8 DISPIDS.H--Dispid for CaptionMethod #define dispidCaptionMethod 2 Next you need to add the new method to your ODL file. From the Project Workspace window, select the File View, and open the BCFControl.odl file. Listing 10.9 shows the method that you added to the BCFControl's primary dispatch interface. The dispidCaptionMethod constant is used for the id of the method, the return type is HRESULT--because it's dual-interface, and the name is CaptionMethod. The parameters are de-fined as a BSTR, named lpctstrCaption, and a VARIANT, named varAlignment. Your last parameter is actually your return value and is defined as a long pointer, named RetVal. You also added the direction that the parameters flow in the form of [in] and [out] parameter attributes. (See Table 10.2 for a complete description of the possible attributes that can be used.) Parameter attributes are used to aid development tools, such as VB, in determining how parameters are used within a function call. A tool like VB will hide the details of how parameters are handled--for example, creating and destroying memory--based on the parameter direction attributes and other attributes in the type library. The type library is the description of your component to the tools that will use it and is why it is so important to ActiveX component development. Table 10.2 Parameter Flow Attributes Direction
Description
in
Parameter is passed from caller to callee.
out
Parameter is returned from callee to caller.
in, out
Parameter is passed from caller to callee, and the callee returns a parameter.
out, retval
Parameter is the return value of the method and is returned from callee to the caller.
Next compile the type library to create a new C++ header file defining your control and its interfaces. After compiling, open the BCFControlInterfaces.h file, and copy the CaptionMethod line.
Listing 10.9 BCFCONTROLINTERFACES.H--Interface File Created from the ODL File interface DECLSPEC_UUID("317512F1-3E75-11d0-BEBE-00400538977D") IBCFControl : public IDispatch { public: virtual /* [id] */ HRESULT STDMETHODCALLTYPE CaptionMethod( /* [in] */ BSTR bstrCaption, /* [optional][in] */ VARIANT varAlignment, /* [retval][out] */ long __RPC_FAR *lRetVal) = 0;
}; Open the BCFControlCtl.h file, and paste the line into your class header file, making sure that you remove the = 0 from the prototype. To aid your implementation, you also need to add an enumeration and two member variables to your class definition (see Listing 10.10).
Listing 10.10 BCFCONTROLCTL.H--Updated Control Class Header File ... #include "BCFControlInterfaces.H" #include "Dispids.H" #include "alignmentenums.h" typedef struct tagBCFCONTROLCTLSTATE { long lCaptionLength; long lAlignment; } BCFCONTROLCTLSTATE; //=-------------------------------------------------------------------------= // CBCFControlControl //=-------------------------------------------------------------------------= // our control. // class CBCFControlControl : public COleControl, public IBCFControl, public ISupportErrorInfo { public: // IUnknown methods // DECLARE_STANDARD_UNKNOWN(); // IDispatch methods // DECLARE_STANDARD_DISPATCH(); // ISupportErrorInfo methods // DECLARE_STANDARD_SUPPORTERRORINFO(); // IBCFControl methods // // TODO: copy over the method declarations from BCFControlInterfaces.H // don't forget to remove the PURE from them. // STDMETHOD(CaptionMethod)(THIS_ BSTR bstrCaption, VARIANT varAlignment, long FAR* lRetVal); STDMETHOD_(void, AboutBox)(THIS); // OLE Control stuff follows: // CBCFControlControl(IUnknown *pUnkOuter); virtual ~CBCFControlControl(); // static creation function. all controls must have one of these! //
static IUnknown *Create(IUnknown *); private: // overridables that the control must implement. // STDMETHOD(LoadBinaryState)(IStream *pStream); STDMETHOD(SaveBinaryState)(IStream *pStream); STDMETHOD(LoadTextState)(IPropertyBag *pPropertyBag, IErrorLog *pErrorLog); STDMETHOD(SaveTextState)(IPropertyBag *pPropertyBag, BOOL fWriteDefault); STDMETHOD(OnDraw)(DWORD dvAspect, HDC hdcDraw, LPCRECTL prcBounds, LPCRECTL prcWBounds, HDC hicTargetDev, BOOL fOptimize); virtual LRESULT WindowProc(UINT msg, WPARAM wParam, LPARAM lParam); virtual BOOL RegisterClassData(void); virtual HRESULT InternalQueryInterface(REFIID, void **); virtual BOOL BeforeCreateWindow(DWORD *pdwWindowStyle, DWORD *pdwExWindowStyle, LPSTR pszWindowTitle); // private state information. // BCFCONTROLCTLSTATE m_state; BCFCONTROLCTLSTATE m_DefaultState; protected: // storage variable for the caption LPTSTR m_lptstrCaption; }; ... The enumeration, which is in the include file Alignmentenums.h, is your list of supported alignment styles (see Listing 10.11).
Listing 10.11 ALIGNMENTENUMS.H--Alignment Styles Enumeration typedef enum tagAlignmentEnum { EALIGN_LEFT = 0, EALIGN_CENTER = 1, EALIGN_RIGHT = 2, }EALIGNMENT; #define EALIGN_LEFT_TEXT "Left" #define EALIGN_CENTER_TEXT "Center" #define EALIGN_RIGHT_TEXT "Right" You added the lAlignment member variable into the BCFCONTROLCTLSTATE structure, which is a member variable declared as m_state, in your control class. Define the m_lptstrCaption variable as a member of the control class rather than a member of the state structure--for persistence reasons; for now, suffice it to say that you will make your life a lot easier by declaring your member variables this way. You will address persistence and the use of the state structures in more detail a little later in this chapter in the section "Persistence." Also, declare the member variable, lCaptionLength, which is related to the m_lptstrCaption variable, which is needed for persistence reasons; for now, however, it will serve no purpose. In addition to the m_state variable, you need to add another member of the same type called m_DefaultState. This structure is initialized in the constructor to all of the default values that your control supports (see Listing 10.12). These values will be used later in your persistence implementation to determine
whether the properties have changed from their default values, and if not, the properties will not be persisted. See the section entitled "Persistence" for more information.
Listing 10.12 BCFCONTROLCTL.CPP--Member Variable Initialization //=-------------------------------------------------------------------------= // CBCFControlControl::CBCFControlControl //=-------------------------------------------------------------------------= // "Being born is like being kidnapped. And then sold into slavery." // - andy warhol (1928 - 87) // // Parameters: // IUnknown * - [in] // // Notes: // #pragma warning(disable:4355) // using `this' in constructor CBCFControlControl::CBCFControlControl ( IUnknown *pUnkOuter ) : COleControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this) { // initialize anything here ... // memset(&m_state, 0, sizeof(BCFCONTROLCTLSTATE)); memset(&m_DefaultState, 0, sizeof(BCFCONTROLCTLSTATE)); // NULL terminate the string reference m_lptstrCaption = new TCHAR[1]; m_lptstrCaption[0] = `\0'; // set the alignment to the default of left m_DefaultState.lAlignment = m_state.lAlignment = EALIGN_LEFT; } #pragma warning(default:4355) // using `this' in constructor
NOTE: If you like interesting quotations, you will find that the BaseCtl code is sprinkled with them in appropriate places, as can be seen in Listing 10.12.
The CaptionMethod contains all of the code for getting the caption and the alignment style and, like your MFC and ATL implementations, deals correctly with the optional parameter. Listing 10.13 shows the implementation of the CaptionMethod. Since the method is used both for your IDispatch implementation and your custom interface, it is implemented slightly different from its MFC counterpart. First you declare an HRESULT return type. This return value is used by OLE to determine whether the method call succeeded. The string parameter is passed in differently also. All strings are passed as UNICODE in OLE. This is true even for MFC; the only difference is that MFC hides from the developer the implementation details of how the strings are managed; the developer simply uses the appropriate string data type based on the
target application and platform, that is, Win32 ANSI versus Win32 UNICODE.
Listing 10.13 BCFCONTROLCTL.CPP--CaptionMethod Implementation STDMETHODIMP CBCFControlControl::CaptionMethod(BSTR bstrCaption, VARIANT varAlignment, long FAR * lRetVal) { HRESULT hResult = S_OK; // return value initialized to failure result *lRetVal = FALSE; MAKE_ANSIPTR_FROMWIDE(lpctstrCaption, bstrCaption); // if the variant is a long just use the value if(VT_I4 == varAlignment.vt) { // assign the value to our member variable m_state.lAlignment = varAlignment.lVal; // set the return value *lRetVal = TRUE; } // if the user didn't supply an alignment parameter we will assign the default else if(VT_ERROR == varAlignment.vt || VT_EMPTY == varAlignment.vt) { // assign the value to our member variable m_state.lAlignment = EALIGN_LEFT; // set the return value *lRetVal = TRUE; } else { // get a variant that we can use for conversion purposes VARIANT varConvertedValue; // initialize the variant ::VariantInit(&varConvertedValue); // see if we can convert the data type to something useful // VariantChangeTypeEx() could also be used if(S_OK == ::VariantChangeType(&varConvertedValue, (VARIANT *) &varAlignment, 0, VT_I4)) { // assign the value to our member variable switch(varConvertedValue.lVal) { case EALIGN_CENTER: m_state.lAlignment = EALIGN_CENTER; break; case EALIGN_RIGHT: m_state.lAlignment = EALIGN_RIGHT; break; default: m_state.lAlignment = EALIGN_LEFT; break;
} // set the return value *lRetVal = TRUE; } else { // at this point we could either throw an error // indicating there was a problem converting // the data or change the return type of the method // and return the HRESULT value from the // the "VariantChangeType" call. } } // if everything was OK if(TRUE == *lRetVal) { // if we have a string if(lpctstrCaption != NULL) { // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpctstrCaption) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpctstrCaption); } // did they pass us bad data? if(m_state.lAlignment < EALIGN_LEFT || m_state.lAlignment > EALIGN_RIGHT) // sure did, lets fix their little red wagon m_state.lAlignment = EALIGN_LEFT; // force the control to repaint itself this->InvalidateControl(NULL); // this->InvalidateControl(); = EALIGN_LEFT && Value SetModifiedFlag(); PropertyChanged(dispidAlignment); // this->BoundPropertyChanged(dispidAlignment); CaptionMethod(lpszNewValue, varAlignment, &lRetVal); // use the "CaptionMethod" implementation if(hResult == S_OK && lRetVal) // let the container know that the property has changed m_fDirty = TRUE; // this->SetModifiedFlag(); QueryInterface(IID_IBCFControl, (void **)&pBCFControl); // if it failed then exit if (FAILED(hr)) return FALSE; // get the alignment from the control pBCFControl->get_Alignment(&lAlignment); // set the alignment selection in the control ::SendMessage(::GetDlgItem(hwnd, IDC_ALIGNMENTCOMBO), CB_SETCURSEL, lAlignment, 0); // release the interface pBCFControl->Release(); } return TRUE; case PPM_APPLY: { IBCFControl *pBCFControl; IUnknown *pUnk; HRESULT hr; DWORD dwCookie; // get all the controls we have to update. // for(pUnk = FirstControl(&dwCookie); pUnk; pUnk = NextControl(&dwCookie)) { // QI for the controls custom interface hr = pUnk->QueryInterface(IID_IBCFControl, (void **)&pBCFControl); // if it failed then continue to the next "for" iteration if (FAILED(hr)) continue; // get the alignment selection in the dialog control long lAlignment = ::SendMessage( ::GetDlgItem(hwnd, IDC_ALIGNMENTCOMBO), CB_GETCURSEL, 0,0); // set the alignment in the control
pBCFControl->put_Alignment(lAlignment); // release the interface pointer pBCFControl->Release(); } } return TRUE; case WM_COMMAND: switch (LOWORD(wParam)) { case IDC_ALIGNMENTCOMBO: if(HIWORD(wParam) == CBN_SELCHANGE) this->MakeDirty(); } break; } return FALSE; }
Adding Events Properties and methods are a way for a programmer to communicate with a control from within the container application. Events are a way for the control to communicate with the container. For ActiveX controls, events are nothing more than IDispatch interfaces that are implemented on the container side of the container/control relationship. The mechanism that events are based on is known as a connection point. A connection point is simply a description of the type of interface that is required in order to communicate with the container. Connection points are not restricted to only IDispatch interfaces; rather, they can be of any COM interface that is understood by both components. For that matter, connection points/events are not restricted to only controls, they can be used in any COM implementation. Controls were simply the first to take advantage of them. For more information regarding connection points, refer to the documentation in the OLE online help or to Kraig Brockschmidt's Inside OLE, Second Edition, from Microsoft Press. For the BaseCtl Framework, events are fairly easy to implement. You add an event for notifying the user that the data in your control has changed. Your event will contain two parameters: a string and a long. The parameters are your caption and alignment property values and are passed by reference so that the programmer of the control can allow or disallow the change that may be taking place. This type of event may not be practical but is used to demonstrate the fact that events are nothing more than methods and have the same level of functionality and flexibility. The first step is to add the dispid that you use to identify the event (see Listing 10.27).
Listing 10.27 DISPIDS.H--Change Event Dispid ... #define dispidCaptionProp 3 // events //
#define eventidChange 5 #define _DISPIDS_H_ #endif // _DISPIDS_H_ As with every other aspect of your control implementation, you must add an entry to the ODL file. Events are added in the same fashion as methods; however, they are added to the event dispatch interface in the ODL file (see Listing 10.28). Note that the event interface differs slightly from your primary dispatch interface. The most obvious difference being that the interface is of type dispinterface and does not support dual-interface. For these reasons, your event methods are declared in C/C++ fashion and do not require the parameter attributes that your primary dispatch interface did. NOTE: Event (or rather Source) interfaces cannot be dual-interface.
Listing 10.28 BCFCONTROL.ODL--Event ODL Entry // event interface for CBCFControl controls ... // [ uuid(317512F2-3E75-11d0-BEBE-00400538977D), helpstring("Event interface for BCFControl control"), hidden ] dispinterface DBCFControlEvents { properties: methods: [id(eventidChange)] void Change(BSTR* cstrCaption, long* lAlignment); }; When you implemented your methods and properties, you had to copy the function prototype from the C++ header file that was generated from the ODL file to your class definition. For events, the implementation works a little differently. The function for the event is actually located in the container, so instead of implementing the function, you need to create some utility structures for calling the event (see Listing 10.29). You added an enumeration to identify the event that you want to fire. You also declared two arrays: one of VARTYPE and the other EVENTINFO. The VARTYPE array is an array of variant data type constants that are passed to the FireEvent function. The array is used to identify and give the order of the parameters (if any) that need to be passed to the event method when it is called. The EVENTINFO array is an array of structures. The EVENTINFO is defined by the BaseCtl framework and is used for declaring the event, the number of parameters it has, and the VARTYPE array of parameter types that it uses. NOTE: When assigning the values to the enumeration, it is important that they correspond to the position in the EVENTINFO structure array of the actual event being referenced. The first event is at
position 0 in the array, the second is at position 1 in the array, and so forth. The enumeration value is used in the event firing method to retrieve information about the event and how it is called.
Listing 10.29 BCFCONTROLCTL.H--Event Handling Structures--Header File ... #include "alignmentenums.h" typedef enum { BCFControlEvent_Change = 0, } BCFCONTROLEVENTS; VARTYPE rgPI4PBSTR []; EVENTINFO m_rgBCFControlEvents[]; typedef struct tagBCFCONTROLCTLSTATE { ... You've added your header declarations; now you need to add your source file implementation (see Listing 10.30). Here you initialize the two arrays that you declare in your header file. The VARTYPE array is initialized with two elements: a BSTR pointer type and a long pointer type. These are the data types of the two parameters that are passed to the event method. The EVENTINFO structure is initialized to a single element identifying the event ID, the number of parameters, and the parameter array descriptor.
Listing 10.30 BCFCONTROLCTL.CPP--Event Structures Implementation-Source File ... //=-------------------------------------------------------------------------= // all the events in this control // // TODO: add events here ... // VARTYPE rgPI4PBSTR [] = { VT_BSTR | VT_BYREF, VT_I4 | VT_BYREF }; EVENTINFO m_rgBCFControlEvents [] = { { eventidChange, 2, rgPI4PBSTR } }; ... Your event interface is now completely implemented. The final step is only a matter of adding the FireEvent method calls wherever appropriate in your control implementation. Before adding the FireEvent calls to your code, though, you add a simple helper function to aid your implementation. Since your implementation of the FireEvent method allows the user of the control to change the data that is passed to the event, you will find it easier to maintain the code by implementing the simple helper function, FireChange (with no parameters), that
encapsulates the data management associated with the method and its parameters (see Listing 10.31).
Listing 10.31 BCFCONTROLCTL.H--FireChange Event--Header File ... // private state information. // BCFCONTROLCTLSTATE m_state; protected: void FireChange(void); LPTSTR m_lptstrCaption; }; ... Now add your implementation (see Listing 10.32). The BaseCtl framework defines the FireEvent function for calling event functions. The first parameter of the function is the EVENTINFO structure that you defined, and the remaining parameters are based on the parameters you declared for the event.
Listing 10.32 BCFCONTROLCTL.CPP--FireChange Event Implementation void CBCFControlControl::FireChange(void) { // get a BSTR that can be passed via the event BSTR bstrCaption = ::SysAllocString(OLESTRFROMANSI(m_lptstrCaption)); // fire the change event this->FireEvent(&(m_rgBCFControlEvents[BCFControlEvent_Change]), &bstrCaption, &m_state.lAlignment); // create an ANSI string MAKE_ANSIPTR_FROMWIDE(lpctstrCaption, bstrCaption); // free the data that was passed back ::SysFreeString(bstrCaption); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpctstrCaption) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpctstrCaption); } Now you add the event firing to your control, which is just a matter of adding the this->FireChange(); function call wherever appropriate. In your case, you added it to your put_Alignment and CaptionMethod functions. Last you compile the control and test your implementation.
TIP: If any of the methods, properties, or events don't appear in your container after you have added them, remember to do the following: ❍ ❍ ❍ ❍
Delete the .tlb file for the control, and look for extra copies in other directories. Update the dependencies of your project. Rebuild your control. Register the control.
Occasionally, the project will point to an incorrect version of the type library or the registry may point to the incorrect version of the control, which can be a little confusing when you're testing your control and it doesn't behave the way you expect.
Persistence Persistence refers to the capability of a component to retain its state across execution lifetimes. In other words, regardless of the number of times that the control is started and stopped, it remembers that you changed its background color from white to mauve. Persistence in a BaseCtl implementation is broken into two major parts consisting of a total of four aspects. The first part is described as text persistence, that is the persistence of the properties to some form of permanent storage such as a file. The second part, known as binary persistence, is when the control is being used in some form of development environment and the state of the container is switching between a design-time mode and a runtime mode for testing purposes. In this case, the persistence is performed using a temporary storage device such as the computer's memory. Text persistence of the control's properties are broken into two parts, also. The first is the function LoadTextState, which is called the first time the control is instantiated. Note that this function will not be called when switching from a design-time mode to a runtime mode, even though the control is re-created when switching between modes. The last function to be called is SaveTextState, which is called when the control instance is being terminated. For an application that is switching between design-time and runtime mode, this function will not be called, even though the control is destroyed and re-created. Binary persistence also consists of two parts: LoadBinaryState and SaveBinaryState. SaveBinaryState is called when a control, in design-mode, is destroyed with the intent of switching to runtime mode. LoadBinaryState is called when the control is being loaded in design-time mode after being in runtime mode, and vice versa. The hierarchy of events looks like this: Runtime mode only: LoadText Data ... (some action) SaveTextState Design-time mode and runtime mode:
LoadTextState ... SaveBinaryState LoadBinaryState ... LoadBinaryState ... SaveTextState -
Design Mode (Container is changed to Run-Time mode) (Container is changed to Run-Time mode) (Container is changed to Design mode) Design Mode
First you implement your text persistence, and then you implement your binary persistence.
Text Persistence Text persistence is the actual storage of your data to some form of data store, such as a file. For your implementation, you persist the caption, alignment, text data path, and background color. LoadTextState Listing 10.33 shows the implementation for your loading of the properties. The control simply reads the property from the stream and, if successful, loads the property into the control.
NOTE: Do not exit this function if the control was unable to retrieve a property. It may be that the property was not persisted in the first place and does not really represent an error.
Listing 10.33 BCFCONTROLCTL.CPP--LoadTextState Implementation STDMETHODIMP CBCFControlControl::LoadTextState ( IPropertyBag *pPropertyBag, IErrorLog *pErrorLog ) { HRESULT hr; VARIANT v; ::VariantInit(&v); v.vt = VT_BSTR; hr = pPropertyBag->Read(OLESTRFROMANSI("Caption"), &v, pErrorLog); if(SUCCEEDED(hr)) { // get a ANSI string from the BSTR MAKE_ANSIPTR_FROMWIDE(lpctstrCaption, v.bstrVal); // free the BSTR that was passed in ::SysFreeString(v.bstrVal); // if we have a string if(lpctstrCaption != NULL) { // if we have a string if(m_lptstrCaption) { // delete the existing string
delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpctstrCaption) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpctstrCaption); } } ::VariantInit(&v); v.vt = VT_I4; hr = pPropertyBag->Read(OLESTRFROMANSI("Alignment"), &v, pErrorLog); if(SUCCEEDED(hr)) m_state.lAlignment = v.lVal; ::VariantInit(&v); v.vt = VT_I4; hr = pPropertyBag->Read(OLESTRFROMANSI("BackColor"), &v, pErrorLog); if(SUCCEEDED(hr)) m_state.ocBackColor = v.lVal; this->InvalidateControl(NULL); this->m_fDirty = TRUE; // always return S_OK return S_OK; } SaveTextState Listing 10.34 shows your persistence to permanent storage implementation. Note that the values are persisted only if they have not changed from their default value or are required to by the container. The use of the default state structure definitely improves your overall control load and save times, which is critical to having a control that is useful.
Listing 10.34 BCFCONTROLCTL.CPP--SaveTextState Implementation STDMETHODIMP CBCFControlControl::SaveTextState ( IPropertyBag *pPropertyBag, BOOL fWriteDefaults ) { HRESULT hr = S_OK; VARIANT v; if(lstrlen(m_lptstrCaption) || fWriteDefaults) { ::VariantInit(&v); v.vt = VT_BSTR; v.bstrVal = ::SysAllocString(OLESTRFROMANSI(m_lptstrCaption)); if (!v.bstrVal) return E_OUTOFMEMORY; hr = pPropertyBag->Write(OLESTRFROMANSI("Caption"), &v); ::SysFreeString(v.bstrVal); RETURN_ON_FAILURE(hr);
} if(m_DefaultState.lAlignment != m_state.lAlignment || fWriteDefaults) { ::VariantInit(&v); v.vt = VT_I4; v.lVal = m_state.lAlignment; hr = pPropertyBag->Write(OLESTRFROMANSI("Alignment"), &v); RETURN_ON_FAILURE(hr); } if(m_DefaultState.ocBackColor != m_state.ocBackColor || fWriteDefaults) { ::VariantInit(&v); v.vt = VT_I4; v.lVal = m_state.ocBackColor; hr = pPropertyBag->Write(OLESTRFROMANSI("BackColor"), &v); RETURN_ON_FAILURE(hr); } return hr; } You've implemented the persistence of the control's data for the start and end of its lifetime. Now you need to implement the persistence of the control when it transitions from a design-time mode to a runtime mode and back again.
Binary Persistence Binary persistence is the streaming of the control's data to a temporary storage device, which is done for performance and storage reasons. Loading the data of the control in and out of the binary persistence is much faster than writing to and from the primary storage. In addition, until the container of the control is completely closed, the persistent data of the control should be considered transitive and volatile and should not be persisted to permanent storage. You probably don't want to save the properties of the control to a file every time you start or stop your application while in design mode. Throughout the implementation of various other aspects of your control, you have been adding your data members to a structure called m_state. Now that structure becomes of real use. SaveBinaryState Listing 10.35, shows your implementation of the SaveBinaryState method. First you determine the length of the string m_lptstrCaption and store it in your state structure m_state. Next you persist the state structure in its entirety. Finally you persist the two strings. You do this in separate operations because the strings can be of variable length, and you do not want to restrict the user in any way by truncating the strings or enforcing a size limit.
Listing 10.35 BCFCONTROLCTL.CPP--SaveBinaryState Implementation STDMETHODIMP CBCFControlControl::SaveBinaryState ( IStream *pStream ) { HRESULT hr = S_OK;
// store the length of the string and the NULL m_state.lCaptionLength = lstrlen(m_lptstrCaption) + 1; // write the state of the data to the stream hr = pStream->Write(&m_state, sizeof(m_state), NULL); RETURN_ON_FAILURE(hr); // write the string and the NULL hr = pStream->Write(m_lptstrCaption, m_state. lCaptionLength, NULL); RETURN_ON_FAILURE(hr); return S_OK; } After you persist the data, all that remains is to read the data back in when the time is right. LoadBinaryState When the container changes its state from runtime mode to design-time mode (and vice versa), the control is destroyed and re-created. However, in the interests of performance, the control will read its persistence from a local stream set up by the container, rather than from the normal storage used by the container when persisting the properties across execution boundaries. LoadBinaryState receives an IStream pointer as its only parameter, from which you can read your persisted data. Listing 10.36 shows your implementation of the LoadBinaryState function. First you read in your m_state structure, which allows you to determine the length of the two strings that were persisted with the SaveBinaryState function. After you read in your strings, you force the control to redraw itself to reflect the new information.
Listing 10.36 BCFCONTROLCTL.CPP--LoadBinaryState Implementation STDMETHODIMP CBCFControlControl::LoadBinaryState ( IStream *pStream ) { HRESULT hr = S_OK; // read the state of the control hr = pStream->Read(&m_state, sizeof(m_state), NULL); // create a string of the appropriate size this includes the NULL m_lptstrCaption = new TCHAR[m_state.lCaptionLength]; // read the string and NULL hr = pStream->Read(m_lptstrCaption, m_state.lCaptionLength, NULL); // redraw the control this->InvalidateControl(NULL); this->m_fDirty = TRUE; return S_OK; } Although persistence support requires a little more work when using the BaseCtl framework, it is not too difficult. It is more tedious and time consuming than anything. The BaseCtl sample code contains examples of persisting other type of properties, including fonts, which differ slightly from other built-in data types, such as long or BSTR.
Drawing the Control
You draw the control's UI similarly to the way you draw it in your MFC and ATL implementations. For developers accustomed to MFC, the most difficult aspect of drawing the UI is the lack of utility class support. All those nice classes and functions that create brushes and convert colors, for example, are not available with the BaseCtl framework. Most MFC classes and functions have Win32 equivalents, so you shouldn't find it too difficult to convert between the two. As we point out in Chapters 6 and 8, there are two types of drawing: standard and optimized. Your drawing implementation will support both methods. This chapter will describe standard drawing, and Chapter 11 will focus on optimized drawing.
Standard Drawing Standard drawing is just that: standard. You have complete freedom to draw the control any way you see fit, using any method that is appropriate. You can use pens and brushes, rectangles and circles. Remember that drawing smart is the goal of any application with UI. First you need to add a number of member variables and functions to aid in your drawing implementation (see Listing 10.37).
Listing 10.37 BCFCONTROLCTL.H--Drawing Implementation Member Variables and Functions ... virtual HRESULT InternalQueryInterface(REFIID, void **); virtual BOOL BeforeCreateWindow(DWORD *pdwWindowStyle, DWORD *pdwExWindowStyle, LPSTR pszWindowTitle); // OnData is called asynchronously as data for an object // or property arrives... virtual HRESULT OnData(DISPID propId, DWORD bscfFlag, IStream * strm, DWORD dwSize); // private state information. // BCFCONTROLCTLSTATE m_state; BCFCONTROLCTLSTATE m_DefaultState; protected: void FireChange(void); LPTSTR m_lptstrCaption; IFont * m_pFont; void LoadFont(void); HBRUSH hBrush, hOldBrush; COLORREF TranslateColor(OLE_COLOR clrColor, HPALETTE hpal = NULL){ COLORREF cr = RGB(0x00,0x00,0x00);OleTranslateColor(clrColor, hpal, &cr);return cr;} void FillSolidRect(HDC hDC, LPCRECT lpRect, COLORREF clr){ ::SetBkColor(hDC, clr);::ExtTextOut(hDC, 0, 0, ETO_OPAQUE, lpRect, NULL, 0, NULL);} void GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy); BOOL bRetrievedDimensions; int iCharWidthArray[256]; int iCharacterSpacing, iCharacterHeight;
}; ... You need to initialize the member variables to a valid state, which you do in your constructor (see Listing 10.38).
Listing 10.38 BCFCONTROLCTL.CPP--Member Initialization #pragma warning(disable:4355) // using `this' in constructor CBCFControlControl::CBCFControlControl ( IUnknown *pUnkOuter ) : CInternetControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this) { // initialize anything here ... // . . . // clear the font m_pFont = NULL; // clear the brush hOldBrush = hBrush = NULL; // clear the flag bRetrievedDimensions = FALSE; } #pragma warning(default:4355) // using `this' in constructor Next you add the helper functions, GetTextExtent and LoadFont, to your implementation (see Listing 10.39). You also add a default FONTDESC structure in the event that you can't retrieve the ambient font from the container. GetTextExtent is a function that is supported in MFC but not in Win32, so we've created our own implementation. The function determines the width and height of the font of the current Device Context (DC) and then calculates the size in points of the string that was supplied to the function. This function is used for displaying the text with the correct alignment: left, right, or center. We've optimized the method so as to retrieve the information only once. If your control supports fonts for properties, it is a simple matter to clear the flag bRetrievedDimensions to refresh the width and height of the new font when the control redraws itself. The function LoadFont tries to load the ambient font from the container and, if unable, creates a new font from your default settings.
Listing 10.39 BCFCONTROLCTL.CPP--Drawing Helper Functions static FONTDESC _fdDefault = { sizeof(FONTDESC), L"MS Sans Serif", FONTSIZE(8), FW_NORMAL, DEFAULT_CHARSET, FALSE, FALSE,
FALSE }; void CBCFControlControl::LoadFont(void) { // if there isn't a font object if(!m_pFont) // get the font from the container this->GetAmbientFont(&m_pFont); // if there still isn't a font object if(!m_pFont) // create a default font object ::OleCreateFontIndirect(&_fdDefault, IID_IFont, (void **) &m_pFont); } void CBCFControlControl::GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy) { // if we haven't gotten the dimensions yet if(!bRetrievedDimensions) { // get all of the widths for all of the chars ::GetCharWidth(hDC, 0, 255, &iCharWidthArray[0]); // get the spacing between the chars iCharacterSpacing = ::GetTextCharacterExtra(hDC); // make sure that this only executes once bRetrievedDimensions = TRUE; // get the metrics of this DC TEXTMETRIC tmMetrics; ::GetTextMetrics(hDC, &tmMetrics); // get the height iCharacterHeight = tmMetrics.tmHeight; } // return the height cy = iCharacterHeight; // set the initial value to 0 int iTextWidth = 0; // get the number of characters in our string long lTextLength = lstrlen(lpctstrString); // if we have a character if(lTextLength) { long lEndCharPos = lTextLength - 1; // add up the widths of the characters and the spacing for(long lCount = 0; lCount LoadFont(); if(m_pFont) { // get a font handle m_pFont->get_hFont(&hFont); // increment the ref count so the font doesn't drop // out from under us m_pFont->AddRefHfont(hFont); ::SelectObject(hdcDraw, hFont); } // ** // ****** Get the text font ****** // ****** Get the colors ****** // ** // use the window color as the background color OLE_COLOR tColor; this->get_BackColor(&tColor); COLORREF clrTextBackgroundColor = this->TranslateColor(tColor); // then use the normal windows color for the text COLORREF clrTextForegroundColor =
this->TranslateColor(::GetSysColor(COLOR_WINDOWTEXT)); // set to the system color COLORREF clrEdgeBackgroundColor = ::GetSysColor(COLOR_3DFACE); COLORREF clrEdgeForegroundColor = ::GetSysColor(COLOR_3DFACE); // ** // ****** Get the colors ****** // ****** Draw the background ****** // ** // set the text color COLORREF clrOldBackgroundColor = ::SetBkColor(hdcDraw,clrTextBackgroundColor); COLORREF clrOldForegroundColor = ::SetTextColor(hdcDraw, clrTextForegroundColor); // if we don't have a brush if(hBrush == NULL) // create a solid brush hBrush = ::CreateSolidBrush(clrTextBackgroundColor); // select the brush and save the old one hOldBrush = ::SelectObject(hdcDraw, hBrush); // draw the background ::Rectangle(hdcDraw, prcBounds->left, prcBounds->top, prcBounds->right, prcBounds->bottom); // ** // ****** Draw the background ****** // ****** Draw the text ****** // ** int iHor, iVer; // get the size of the text for this DC int cx = 0, cy = 0; this->GetTextExtent(hdcDraw, m_lptstrCaption, cx, cy); switch(m_state.lAlignment) { case EALIGN_CENTER: iHor = (prcBounds->right - cx) / 2; iVer = prcBounds->top + 3; break; case EALIGN_RIGHT: iHor = prcBounds->right - cx - 3; iVer = prcBounds->top + 3; break; // case EALIGN_LEFT: default: iHor = prcBounds->left + 3; iVer = prcBounds->top + 3; break; } // output our text ::ExtTextOut(hdcDraw, iHor, iVer, ETO_CLIPPED | ETO_OPAQUE, (LPCRECT) prcBounds, m_lptstrCaption, lstrlen(m_lptstrCaption), NULL); // ** // ****** Draw the text ****** // ****** Draw the border ******
// ** // set the edge style and flags UINT uiBorderStyle = EDGE_SUNKEN; UINT uiBorderFlags = BF_RECT; // set the edge color ::SetBkColor(hdcDraw, clrEdgeBackgroundColor); ::SetTextColor(hdcDraw, clrEdgeForegroundColor); // draw the 3D edge ::DrawEdge(hdcDraw, (LPRECT)(LPCRECT) prcBounds, uiBorderStyle, uiBorderFlags); // ** // ****** Draw the border ****** // ****** Reset the colors ****** // ** // restore the original colors ::SetBkColor(hdcDraw, clrOldBackgroundColor); ::SetTextColor(hdcDraw, clrOldForegroundColor); // ** // ****** Reset the colors ****** // ****** release the text font ****** // ** if(hOldFont) // select the old object ::SelectObject(hdcDraw, hOldFont); // increment the ref count so the font doesn't drop // out from under us if(m_pFont && hFont) m_pFont->ReleaseHfont(hFont); // ** // ****** Get the text font ****** // select the old brush back ::SelectObject(hdcDraw, hOldBrush); // destroy the brush we created ::DeleteObject(hBrush); // clear the brush handles hBrush = hOldBrush = NULL; return S_OK; }
From Here... This chapter focused on creating a basic control implementation. You added methods, properties, and events, which are the backbone of every control implementation. Adding these basic features to a BaseCtl ActiveX control implementation is fairly straightforward. The one major drawback, though, is the lack of IDE support such as you have with MFC and ATL. This chapter also addressed the issues of persistence and drawing without which a control implementation is definitely incomplete. Chapter 11 expands upon what you have learned and adds new features and functions to your control implementation to make your control truly unique and interesting. The BaseCtl framework is fairly complete and robust, and as you progress through Chapter 11, you will find that a
lot of the new features you add will be implemented in a fashion similar to the features you implemented in this chapter. The one thing that BaseCtl has going for it is that it is all based on COM, which is the foundation of any ActiveX component.
Chapter 11 Advanced ActiveX Control Development with BaseCtl ●
Advanced ActiveX Control Development with BaseCtl ❍ Creating Properties ■ Creating Asynchronous Properties ■ Listing 11.1 BCFCONTROLCTL.H--Modified BCFControlControl Class Definition ■ Listing 11.2 BCFCONTROLCTL.CPP--BCFControlControl Constructor Implementation Change ■ Listing 11.3 BCFCONTROLCTL.CPP--OnData Function ■ Listing 11.4 DISPIDS.H--Add Data Path Property Dispid ■ Listing 11.5 BCFCONTROL.ODL--BCFControl ODL Implementation ■ Listing 11.6 BCFCONTROLCTL.H--ReadyState Property Prototype ■ Listing 11.7 BCFCONTROLCTL.CPP--Member Initialization ■ Listing 11.8 BCFCONTROLCTL.CPP--Property Implementation ■ Listing 11.9 BCFCONTROLCTL.CPP--OnData Implementation ■ Static and Dynamic Property Enumeration ■ Listing 11.10 PERPROPERTYBROWSING.H--IPerPropertyBrowsing Interface Macro ■ Listing 11.11 BCFCONTROLCTL.H--IPerPropertyBrowsing Interface Declaration ■ Listing 11.12 BCFCONTROLCTL.CPP--QueryInterface Implementation of IPerPropertyBrowsing ■ Listing 11.13 BCFCONTROLCTL.CPP--IPerPropertyBrowsing Implementation ❍ Drawing the Control ■ Optimized Drawing ■ Listing 11.14 BCFCONTROLCTL.CPP--Optimized Drawing ■ Listing 11.15 BCFCONTROLCTL.CPP--BeforeDestroyWindow Implementation ❍ Adding Clipboard and Drag and Drop Support ■ Clipboard Support ■ Listing 11.16 IDATAOBJECT.H--IDataObject Interface Macro ■ Listing 11.17 IENUMFORMATETC--IEnumFORMATETC Interface Macro ■ Listing 11.18 BCFCONTROLCTL.H--Clipboard Support Implementation-Header File ■ Listing 11.19 BCFCONTROLCTL.CPP--Constructor Member Initialization ■ Listing 11.20 BCFCONTROLCTL.CPP--QueryInterface Update ■ Listing 11.21 BCFCONTROLCTL.CPP--WM_KEYDOWN Message Handler
Listing 11.22 BCFCONTROLCTL.CPP--CopyDataToClipboard Implementation ■ Listing 11.23 BCFCONTROLCTL.CPP--PrepareDataForTransfer Implementation ■ Listing 11.24 BCFCONTROLCTL.CPP--CopyStgMedium Implementation ■ Listing 11.25 BCFCONTROLCTL.CPP--IDataObject Implementation ■ Listing 11.26 BCFCONTROLCTL.CPP--IEnumFORMATETC Implementation ■ Listing 11.27 BCFCONTROLCTL.CPP--OnKeyDown Implementation ■ Listing 11.28 BCFCONTROCTL.H--Clipboard Target Implementation-- Header File ■ Listing 11.29 BCFCONTROLCTL.CPP--GetDataFromClipboard Implementation ■ Listing 11.30 BCFCONTROLCTL.CPP--GetDataFromTransfer Implementation ■ Listing 11.31 BCFCONTROCTL.CPP--OnKeyDown Implementation ■ Adding Drag and Drop Support ■ Listing 11.32 IDROPSOURCE.H--IDropSource Interface ■ Listing 11.33 BCFCONTROCTL.H--IDropSource Interface Implementation ■ Listing 11.34 BCFCONTROLCTL.CPP--QueryInterface Update ■ Listing 11.35 BCFCONTROLCTL.CPP--WindowProc Implementation ■ Listing 11.36 BCFCONTROLCTL.CPP--Drop Source Implementation ■ Listing 11.37 IDROPTARGET.H--IDropTarget Interface ■ Listing 11.38 BCFCONTROLCTL.H--IDropTarget Implementation ■ Listing 11.39 BCFCONTROLCTL.CPP--QueryInterface Update ■ Listing 11.40 BCFCONTROLCTL.CPP--AfterCreateWindow Implementation ■ Listing 11.41 BCFCONTROLCTL.CPP--IDropTarget Implementation ■ Custom Clipboard and Drag and Drop Formats ■ Listing 11.42 BCFCONTROLCTL.H--Custom Data Format Member Variables ■ Listing 11.43 BCFCONTROLCTL.CPP--Register the Custom Format ■ Listing 11.44 BCFCONTROCTL.CPP--PrepareDataForTransfer Update ■ Listing 11.45 BCFCONTROLCTL.CPP--GetDataFromTransfer Update ■ Listing 11.46 BCFCONTROLCTL.CPP--IEnumFORMATETC::Next Update ■ Listing 11.47 BCFCONTROLCTL.CPP--IEnumFORMATETC::GetData Update Subclassing Existing Windows Controls ■ Listing 11.48 BCFCONTROLSUBCTL.CPP--RegisterClassData Implementation Dual-Interface Controls Other ActiveX Features ■ Windowless Activation ■ Listing 11.49 BCFCONTROLCTL.H--BCFControControl ActiveX Implementation ■ Listing 11.50 BCFCONTROLNOWIN.H--BCFControlNoWinControl ActiveX Implementation ■ Unclipped Device Context ■
❍
❍ ❍
Flicker-Free Activation ■ Mouse Pointer Notifications When Inactive ■ Listing 11.51 BCFCONTROLNOWIN.H--Mouse Notifications ■ Optimized Drawing Code ■ Loads Properties Asynchronously From Here... ■
❍
Advanced ActiveX Control Development with BaseCtl ●
●
●
●
●
●
Asynchronous properties Supporting asynchronous properties in the BaseCtl sample shows what is happening behind the scenes when properties are loaded asynchronously. Property enumeration Property enumeration is exposed through a simple COM interface, which is easy to add given the BaseCtl architecture. Optimized drawing Optimized drawing has positive effects on the performance of the control. Like MFC and ATL, BaseCtl support of optimized drawing is trivial. Clipboard and Drag and Drop Adding Clipboard and Drag and Drop support to your BaseCtl implementation is similar to the MFC and ATL implementations. Windows and dual-interface controls Subclassing an existing Windows control reduces development time when creating new control implementations. Like ATL, BaseCtl ActiveX control implementations support dual-interface by default. Advanced ActiveX The BaseCtl framework supports the advanced ActiveX features in this chapter.
This chapter expands upon the information in Chapter 10 about creating a basic BaseCtl ActiveX control, so reading Chapter 10 prior to this chapter is necessary. In addition to the features that you are familiar with, such as Clipboard and Drag and Drop support, you will learn how to implement asynchronous properties and optimized drawing, which are the result of the adoption of OC 96 specification.
Creating Properties Chapter 10 tells you how to add the various types of properties to your control implementation. One type of property has yet to be examined: asynchronous properties.
Creating Asynchronous Properties Asynchronous properties are those properties that typically represent a large amount of data, such as a text or bitmap file, and are loaded as a background process so as not to interfere with the normal processing of the control and the container. This statement can be somewhat misleading. Asynchronous refers only to the call to load the data; it does not refer to the actual loading. For example, a control uses a bitmap as its background and has defined the bitmap as an asynchronous property. If OLE determines that the bitmap is already on the local machine, the data is considered to be available to the control and, subsequently, will instruct the control that all of the data is available. If OLE determines that the bitmap is not available on the local machine, OLE will load the data as fast as possible and inform the control as data becomes available. After the data is in a location that is considered accessible, the property essentially behaves as any other property would. If you require the asynchronous loading of the data regardless of its location, you must implement it yourself. To allow for asynchronous property support, you have to modify your class definition slightly. Listing 11.1 shows the changes that were made to your BCFControlControl class header file. The BaseCtl class COleControl does not provide support for asynchronous properties. You need to take advantage of the BaseCtl class CInternetControl in order to do that. You need to include the Internet.h file and derive the class BCFControlControl from CInternetControl, which is derived from the base class COleControl. You also add the method OnData as your callback function. The callback function is what OLE uses to notify your control that data is being downloaded and is required for asynchronous property support.
Listing 11.1 BCFCONTROLCTL.H--Modified BCFControlControl Class Definition . . . // class declaration for the BCFControl control. // #ifndef _BCFCONTROLCONTROL_H_ #include "IPServer.H" #include "CtrlObj.H" #include "BCFControlInterfaces.H" #include "Dispids.H" #include "internet.h" #include "alignmentenums.h"
typedef struct tagBCFCONTROLCTLSTATE { long lCaptionLength; long lAlignment; OLE_COLOR ocBackColor; } BCFCONTROLCTLSTATE; //=------------------------------------------------------------------------= // CBCFControlControl //=------------------------------------------------------------------------= // our control. // class CBCFControlControl : public CInternetControl, public IBCFControl, public ISupportErrorInfo { . . . // OnData is called asynchronously as data for an // object or property arrives... virtual HRESULT OnData(DISPID propId, DWORD bscfFlag, IStream * strm, DWORD dwSize); // private state information. // BCFCONTROLCTLSTATE m_state; . . . Listing 11.2 shows your changes to the implementation of your constructor to enable asynchronous property support. Your constructor implementation is trivial since your only change is to replace the COleControl constructor declaration with CInternetControl. Note the pearls of wisdom from the authors of the BaseCtl sample.
Listing 11.2 BCFCONTROLCTL.CPP--BCFControlControl Constructor Implementation Change //=------------------------------------------------------------------------= // CBCFControlControl::CBCFControlControl //=------------------------------------------------------------------------= // "Being born is like being kidnapped. And then sold into slavery." // - andy warhol (1928 - 87) // // Parameters: // IUnknown * - [in] //
// Notes: // #pragma warning(disable:4355) // using `this' in constructor CBCFControlControl::CBCFControlControl ( IUnknown *pUnkOuter ) : CInternetControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this) { // initialize anything here ... Listing 11.3 shows the OnData function that you added to your source file. For now, you just add the function shell; you will add the specific implementation after you add your data path property.
Listing 11.3 BCFCONTROLCTL.CPP--OnData Function ... HRESULT CBCFControlControl::OnData(DISPID propId, DWORD bscfFlag, IStream * strm, DWORD dwSize) { HRESULT hr = NOERROR; return(hr); }
NOTE: We experienced some link problems when compiling our BCFControl sample code, as follows: Unresolved external: CreateURLMoniker Unresolved external: RegisterBindStatusCallback Even though the functions are declared in urlmon.h and should be implemented in uuid.lib, we found that we had to link with Urlmon.lib to resolve the functions.
Before you add your specific implementation code to support the asynchronous property, you need to add the stock property ReadyState to your control. You also need to add a user-defined property for your data path variable. This property is used to store the location of the asynchronous properties data. This location can be any valid pathname, including URL and UNC paths. First you need to declare a dispid for your data path property (see Listing 11.4). You use the OLE defined dispid for the ReadyState property.
Listing 11.4 DISPIDS.H--Add Data Path Property Dispid . . . //=------------------------------------------------------------------------= // for the BCFControl control // properties & methods // #define dispidAlignment 1 #define dispidCaptionMethod 2 #define dispidCaptionProp 3 #define dispidTextDataPath 4 . . . Next you add the two new properties to your ODL file (see Listing 11.5). Note that you add only a method for getting the ReadyState property, and not a method for setting the property, which has the effect of creating a read-only property.
Listing 11.5 BCFCONTROL.ODL--BCFControl ODL Implementation . . . [uuid(317512F1-3E75-11d0-BEBE-00400538977D), helpstring("BCFControl Control"), hidden, dual, odl] interface IBCFControl : IDispatch { // properties [id(dispidAlignment), propget] HRESULT Alignment([out, retval] long * Value); [id(dispidAlignment), propput] HRESULT Alignment([in] long Value); [id(DISPID_BACKCOLOR), propget] HRESULT BackColor([out, retval] OLE_COLOR * Value); [id(DISPID_BACKCOLOR), propput] HRESULT BackColor([in] OLE_COLOR Value); [id(DISPID_READYSTATE), propget] HRESULT ReadyState([out, retval] long * Value); [id(dispidTextDataPath), propget] HRESULT TextDataPath([out, retval] BSTR * Value); [id(dispidTextDataPath), propput] HRESULT TextDataPath([in] BSTR Value); // methods . . . You need to add a member variable to your state structure to store the state of the control's asynchronous properties, and you also add a string length member that will be used later in your persistence routines.
You also need to add your function prototypes of the property get/set methods to your class header file (see Listing 11.6). Remember that the prototype is generated automatically when the ODL file is compiled. Finally you add a string member to hold the value of the property.
Listing 11.6 BCFCONTROLCTL.H--ReadyState Property Prototype . . . typedef struct tagBCFCONTROLCTLSTATE { long lCaptionLength; long lAlignment; OLE_COLOR ocBackColor; long lReadyState; long lTextDataPathLength; } BCFCONTROLCTLSTATE; //=------------------------------------------------------------------------= // CBCFControlControl //=------------------------------------------------------------------------= . . . // IBCFControl methods // STDMETHOD(get_Alignment)(THIS_ long FAR* Value); STDMETHOD(put_Alignment)(THIS_ long Value); STDMETHOD(get_BackColor)(THIS_ OLE_COLOR FAR* Value); STDMETHOD(put_BackColor)(THIS_ OLE_COLOR Value); STDMETHOD(get_ReadyState)(THIS_ long FAR* Value); STDMETHOD(get_TextDataPath)(THIS_ BSTR FAR* bstrRetVal);
STDMETHOD(put_TextDataPath)(THIS_ BSTR Value); STDMETHOD(CaptionMethod)(THIS_ BSTR bstrCaption, VARIANT varAlignment, long FAR* lRetVal); STDMETHOD(get_CaptionProp)(THIS_ VARIANT varAlignment, BSTR FAR* bstrRetVal); STDMETHOD(put_CaptionProp)(THIS_ VARIANT varAlignment, BSTR lpszNewValue); STDMETHOD_(void, AboutBox)(THIS); . . . // private state information. // BCFCONTROLCTLSTATE m_state; protected: LPTSTR m_lptstrCaption; LPTSTR m_lptstrTextDataPath; }; Listing 11.7 shows your member variable initialization in your constructor. You don't need to set any default values since the ReadyState will not be persisted across execution lifetimes.
Listing 11.7 BCFCONTROLCTL.CPP--Member Initialization #pragma warning(disable:4355) // using `this' in constructor CBCFControlControl::CBCFControlControl ( IUnknown *pUnkOuter ) : CInternetControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this) { // initialize anything here ...
// ... // set the ready state of the control m_state.lReadyState = READYSTATE_LOADING; // NULL terminate the string reference m_lptstrTextDataPath = new TCHAR[1]; m_lptstrTextDataPath[0] = `\0'; } #pragma warning(default:4355) // using `this' in constructor Last you add your implementation of the ReadyState and TextDataPath methods to your class source file (see Listing 11.8). You initiate the asynchronous download of your data within your put_TextDataPath method. You do this through a call to SetupDownload, where you pass in the path of the data to be downloaded and the dispid of the property that the data is bound to. As data becomes available, your OnData method is called.
Listing 11.8 BCFCONTROLCTL.CPP--Property Implementation STDMETHODIMP CBCFControlControl::get_ReadyState(long * Value) { // make sure that we have a good pointer CHECK_POINTER(Value); // set the return value *Value = m_state.lReadyState; // return the result return S_OK; } STDMETHODIMP CBCFControlControl::get_TextDataPath(BSTR FAR * bstrRetVal) { // if there is a string if(*bstrRetVal); {
// free the string because we are going to replace it ::SysFreeString(*bstrRetVal); // clear the reference just to be safe *bstrRetVal = NULL; } // return the caption as a BSTR *bstrRetVal = ::SysAllocString(OLESTRFROMANSI(m_lptstrTextDataPath)); return S_OK; } STDMETHODIMP CBCFControlControl::put_TextDataPath(BSTR bstrNewValue) { HRESULT hResult = S_OK; // get a ANSI string from the BSTR MAKE_ANSIPTR_FROMWIDE(lpctstrTextDataPath, bstrNewValue); // if we have a string if(lpctstrTextDataPath != NULL) { // if we have a string if(m_lptstrTextDataPath) { // delete the existing string delete [] m_lptstrTextDataPath; // clear the reference just to be safe m_lptstrTextDataPath = NULL; } // allocate a new string m_lptstrTextDataPath = new TCHAR[lstrlen(lpctstrTextDataPath) + 1]; // assign the string to our member variable lstrcpy(m_lptstrTextDataPath, lpctstrTextDataPath); }
// start the asynchronous download of the data this->SetupDownload(OLESTRFROMANSI(m_lptstrTextDataPath), dispidTextDataPath); // let the container know that the property has changed m_fDirty = TRUE; // this->SetModifiedFlag(); Read(lptstrTempBuffer, dwSize, &ulBytesRead); // if we read in any data if(hr == S_OK && ulBytesRead) { // null terminate the amount of data the was actually read in lptstrTempBuffer[ulBytesRead] = `\0'; // get a new buffer with enough space to hold all of the data LPTSTR lptstrNewBuffer = new TCHAR[lstrlen(m_lptstrCaption) + ulBytesRead + 1]; // copy the existing data to the new buffer lstrcpy(lptstrNewBuffer, m_lptstrCaption); // add the new data to the buffer lstrcat(lptstrNewBuffer, lptstrTempBuffer); // remove the existing string delete [] m_lptstrCaption; // copy the data to the buffer m_lptstrCaption = lptstrNewBuffer; // set the dirty flag m_fDirty = TRUE; // redraw the control this->InvalidateControl(NULL); } // if this is our last notification if(bscfFlag & BSCF_LASTDATANOTIFICATION)
{ // set the ready state of the control m_state.lReadyState = READYSTATE_COMPLETE; // set the dirty flag m_fDirty = TRUE; } // return the result return(hr); } Potentially, any type of data can be rendered in this fashion. The BaseCtl framework provides a sample implementation, called WebImage, that demonstrates the rendering of bitmap data progressively as an asynchronous property.
Static and Dynamic Property Enumeration Property enumeration is a way of restricting a property to a specific set of valid values. An example of an enumeration is a property for determining the alignment of a control's displayed text: left-justified, centered, and right-justified, in your case. Another case is a property used to select the different languages a control supports. Language selection properties are good candidates for both a static set, say for English and German, and a dynamic set, say for all the languages on a particular machine. As is pointed out in Chapters 7 and 9, property enumeration adds, with very little effort, a new level of sophistication to your control implementation. Static Property Enumeration Static Property Enumeration for a BaseCtl implemented control is no different than your MFC and ATL implementations. Static enumeration is dependent on the ODL and requires no control code to implement it. See Chapter 7 for the implementation details. Dynamic Property Enumeration As with your MFC and ATL implementations, adding Dynamic Property Enumeration to your BaseCtl implementation is straightforward. Unfortunately, the BaseCtl does not provide the basic OLE interface for Dynamic Property Enumeration support that you found in MFC, so you must add it yourself. Dynamic Property Enumeration is based on the interface IPerPropertyBrowsing. You will create a macro in a style similar to that of the BaseCtl that will provide the necessary code to implement the interface. Listing 11.10 shows the macro and the definition that you added. Essentially, the macro is just a collection of functions that need to be supported in order to use a specific interface. You are not required to implement the interface with a macro as your are doing here. The macro just makes your control class code a little bit easier to read and manage.
Listing 11.10 IPERPROPERTYBROWSING.H-IPerPropertyBrowsing Interface Macro #define DECLARE_STANDARD_PERPROPERTYBROWSING() \ STDMETHOD(MapPropertyToPage)(DISPID Dispid, LPCLSID lpclsid); \ STDMETHOD(GetPredefinedStrings)(DISPID Dispid, CALPOLESTR* lpcaStringsOut,\ CADWORD* lpcaCookiesOut); \ STDMETHOD(GetPredefinedValue)(DISPID Dispid, DWORD dwCookie, VARIANT* lpvarOut); \ STDMETHOD(GetDisplayString)(DISPID Dispid, BSTR* lpbstr); \ In order for your control to support IPerPropertyBrowsing, you need to include the header file for your IPerPropertyBrowsing macro, inherit from the IPerPropertyBrowsing interface, and add the macro to your class declaration (see Listing 11.11).
Listing 11.11 BCFCONTROLCTL.H--IPerPropertyBrowsing Interface Declaration #include "Dispids.H" #include "internet.h" #include "IPerPropertyBrowsing.h" #include "alignmentenums.h" typedef struct tagBCFCONTROLCTLSTATE { long lCaptionLength; long lAlignment; OLE_COLOR ocBackColor; long lReadyState; long lTextDataPathLength; } BCFCONTROLCTLSTATE; //=------------------------------------------------------------------------=
// CBCFControlControl //=------------------------------------------------------------------------= // our control. // class CBCFControlControl : public CInternetControl, public IBCFControl, public ISupportErrorInfo, public IPerPropertyBrowsing { public: // IUnknown methods // DECLARE_STANDARD_UNKNOWN(); // IDispatch methods // DECLARE_STANDARD_DISPATCH(); // ISupportErrorInfo methods // DECLARE_STANDARD_SUPPORTERRORINFO(); // IPerPropertyBrowsing methods // DECLARE_STANDARD_PERPROPERTYBROWSING(); // IBCFControl methods // STDMETHOD(get_Alignment)(THIS_ long FAR* lRetValue); When an application needs to use an interface in a control (or any component for that matter), the application has to call QueryInterface to locate the correct interface pointer within the component. This requirement is also true for the IPerPropertyBrowsing interface. Listing 11.12 shows the change that you must make to your InternalQueryInterface function in order to support the new interface. This change is required because the control will not function correctly without it.
Listing 11.12 BCFCONTROLCTL.CPP--QueryInterface Implementation of IPerPropertyBrowsing HRESULT CBCFControlControl::InternalQueryInterface(REFIID riid, void **ppvObjOut) { IUnknown *pUnk; *ppvObjOut = NULL; // TODO: if you want to support any additional interfaces, then you should // indicate that here. never forget to call COleControl's version in the // case where you don't support the given interface. // if(DO_GUIDS_MATCH(riid, IID_IBCFControl)) pUnk = (IUnknown *)(IBCFControl *)this; else if(DO_GUIDS_MATCH(riid, IID_IPerPropertyBrowsing)) pUnk = (IUnknown *)(IPerPropertyBrowsing *)this; else return COleControl::InternalQueryInterface(riid, ppvObjOut); pUnk->AddRef(); *ppvObjOut = (void *)pUnk; return S_OK; } Your last requirement is to implement the functions of the interface (see Listing 11.13). MapPropertyToPage is not required for your implementation, so you just return the constant E_NOTIMPL. MapPropertyToPage is used to connect the property to a property page that is implemented either in the container or in the control. GetPredefinedStrings is the first function to be called. When this method is called, the dispid of the property that is currently being referenced will be passed in. This method is called for all properties that the control supports, so take care when adding code. If the function is called and it is
determined that the correct property is in context, the control is required to create an array of strings and cookies. A cookie is any 32-bit value that has meaning to the control implementation. The strings are placed in a list from which the user of the control can select the appropriate value to set the property to. In this case, the cookie value that is supplied is also the value that will be stored in the control's property. GetPredefinedValue is the method that is called when the property browser of the container needs the value that is associated with the particular dispid and cookie. The value that is returned will be the actual value that is stored in the property and not the string that was used to represent it. After the property has been set with its value, the property browser calls the function GetDisplayString to get the string that is associated with the current property setting.
NOTE: It may seem a little redundant to have the method GetDisplayString when the property browser already has the string for the value from the GetPredefinedStrings function. The GetDisplayString function can be implemented without implementing the other methods. Implementing GetDisplayString without implementing the other functions in the IPerPropertyBrowsing interface is for those property types that do not use the standard property selection mechanism, for example, font selection, which uses a color selection dialog rather than a list of choices. The name of the font is retrieved via the GetDisplayString function, but the property selection facility is provided through a standard font dialog.
Listing 11.13 BCFCONTROLCTL.CPP--IPerPropertyBrowsing Implementation STDMETHODIMP CBCFControlControl::MapPropertyToPage(DISPID, LPCLSID) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::GetPredefinedStrings(DISPID Dispid, CALPOLESTR * lpcaStringsOut, CADWORD * lpcaCookiesOut) { HRESULT hResult = S_FALSE; // we should have gotten two pointers if we didn't
if((lpcaStringsOut == NULL) || (lpcaCookiesOut == NULL)) // we are out of here return E_POINTER; // if this is the property that we are looking for if(Dispid == dispidAlignment) { ULONG ulElems = 3; // allocate the memory for our string array lpcaStringsOut->pElems = (LPOLESTR *) ::CoTaskMemAlloc(sizeof(LPOLESTR) * ulElems); // if we couldn't allocate the memory if(lpcaStringsOut->pElems == NULL) // were out of here return E_OUTOFMEMORY; // allocate the memory for our cookie array lpcaCookiesOut->pElems = (DWORD*) ::CoTaskMemAlloc(sizeof(DWORD*) * ulElems); // if we couldn't allocate the memory if (lpcaCookiesOut->pElems == NULL) { // free the string array ::CoTaskMemFree(lpcaStringsOut->pElems); // exit the function return E_OUTOFMEMORY; } // store the number of elements in each array lpcaStringsOut->cElems = ulElems; lpcaCookiesOut->cElems = ulElems; // allocate the strings
lpcaStringsOut->pElems[0] = OLESTRFROMANSI(EALIGN_LEFT_TEXT); lpcaStringsOut->pElems[1] = OLESTRFROMANSI(EALIGN_CENTER_TEXT); lpcaStringsOut->pElems[2] = OLESTRFROMANSI(EALIGN_RIGHT_TEXT); // assign the cookie value lpcaCookiesOut->pElems[0] = EALIGN_LEFT; lpcaCookiesOut->pElems[1] = EALIGN_CENTER; lpcaCookiesOut->pElems[2] = EALIGN_RIGHT; hResult = S_OK; } return hResult; } STDMETHODIMP CBCFControlControl::GetPredefinedValue(DISPID Dispid, DWORD dwCookie, VARIANT* lpvarOut) { BOOL bResult = FALSE; // which property is it switch(Dispid) { case dispidAlignment: // clear the variant ::VariantInit(lpvarOut); // set the type to a long lpvarOut->vt = VT_I4; // set the value to the value that was stored with the string lpvarOut->lVal = dwCookie; // set the return value bResult = TRUE;
break; } return bResult; } STDMETHODIMP CBCFControlControl::GetDisplayString(DISPID Dispid, BSTR* lpbstr) { HRESULT hResult = S_FALSE; // which property is it switch(Dispid) { case dispidAlignment: { switch(m_state.lAlignment) { case EALIGN_LEFT: *lpbstr = BSTRFROMANSI(EALIGN_LEFT_TEXT); break; case EALIGN_CENTER: *lpbstr = BSTRFROMANSI(EALIGN_CENTER_TEXT); break; case EALIGN_RIGHT: *lpbstr = BSTRFROMANSI(EALIGN_RIGHT_TEXT); break; } // set the return value hResult = S_OK; }
break; } return hResult; }
Drawing the Control Optimized drawing allows you to create drawing objects, such as pens or brushes. Rather than remove them when you are finished drawing, you can store them as control member variables and use them the next time your control draws itself. The benefit is that you create a pen once for the drawing lifetime of your control, instead of every time it draws. One thing to remember, though, is that optimized drawing does not guarantee performance improvements. It simply supplies a framework for how drawing should be performed and how drawing resources should be used. A poorly written control is still poorly written, no matter how you slice it. Standard and optimized drawings have a single tradeoff, and that is size versus speed. Standard drawing does not require member variables for the drawing objects that are created and used-- thus requiring less instance data but more processing time--whereas optimized code is the opposite. An additional drawback to optimized drawing is that a container may not support it. A control must, at the very least, support standard drawing functionality, deferring to optimized only if it is available. For BaseCtl (like MFC and ATL), the scope of optimized drawing is very narrow compared to the OC 96 specification, but its use can nonetheless result in performance improvements. The OC 96 specification further breaks optimized drawing into what is known as aspects. For more information on aspect drawing, please see the OC 96 specification that ships with the ActiveX SDK.
Optimized Drawing In chapter 10, you learn how to implement standard drawing. In this chapter, you will enhance the original implementation to take advantage of drawing optimization. Listing 11.14 shows the optimized portion of your drawing implementation. If the container doesn't support optimized drawing, you select the original brush back into the Device Context (DC), and you destroy the brush you created. The next time that the OnDraw function is executed, you re-create the brush. When using optimized, you simply reuse the existing brush.
Listing 11.14 BCFCONTROLCTL.CPP--Optimized Drawing . . . // **
if(hOldFont) // select the old object ::SelectObject(hdcDraw, hOldFont); // increment the ref count so the font doesn't drop // out from under us if(m_pFont && hFont) m_pFont->ReleaseHfont(hFont); // ** // ****** Get the text font ****** // The container does not support optimized drawing. if(!fOptimize) { // select the old brush back ::SelectObject(hdcDraw, hOldBrush); // destroy the brush we created ::DeleteObject(hBrush); // clear the brush handles hBrush = hOldBrush = NULL; } return S_OK; } If the container supports optimized drawing, the final implementation detail is to destroy any resources that may still be active, which you do in the BeforeDestroyWindow function. Listing 11.15 shows the implementation that restores the original brush and destroys the brush that you created.
Listing 11.15 BCFCONTROLCTL.CPP--BeforeDestroyWindow Implementation void CBCFControlControl::BeforeDestroyWindow(void) {
// if there is an old brush if(hOldBrush) { // get the DC HDC hDC = this->OcxGetDC(); // select the old brush back ::SelectObject(hDC, hOldBrush); // release the DC this->OcxReleaseDC(hDC); } // if we created a brush if(hBrush) // destroy the brush we created ::DeleteObject(hBrush); } The fact is the user will not care how great your code is written or how many whiz-bang features it supports if it doesn't draw well. You'll be wise to spend some time on your drawing implementation and get it right.
Adding Clipboard and Drag and Drop Support The basic OLE Clipboard and Drag and Drop interfaces are not implemented within the BaseCtl framework. As with the IPerPropertyBrowsing interface (see the section "Dynamic Property Enumeration"), you must implement the required interfaces yourself. As is pointed out in Chapters 7 and 9, Clipboard and Drag and Drop support can add much to your control implementation, while requiring very little work.
Clipboard Support Clipboard support is based on the IDataObject and IEnumFORMATETC interfaces. IDataObject is the interface through which the data is retrieved from your object when it has been placed on the Clipboard, and IEnumFORMATETC is the interface that is used by an application to determine what types of data your IDataObject interface supports. Using Built-In Clipboard Formats As is pointed out in Chapter 7, the Windows operating system
(OS) supports a number of built-in formats for transferring data via the Clipboard. Your first implementation will be to transfer your caption using the CF_TEXT format, which is the built-in format for transferring ANSI text. There are two aspects to using the Clipboard: being a Clipboard source and being a Clipboard target. You will first look at enabling your control as a Clipboard source. Enabling a Control as a Clipboard Source A Clipboard source is an application that puts data on the Clipboard for other applications to copy. You need to support two interfaces to enable your control as a Clipboard source: IDataObject and IEnumFORMATETC. When the user initiates a data transfer via the Clipboard, a reference to the control's IDataObject interface is placed on the Clipboard. At the time the interface is placed on the Clipboard, you must take a snapshot of the data that the control contains and place it in a STGMEDIUM object. You have to do this because the data may not be copied from the Clipboard immediately and the data needs to reflect the state of the control when the copy operation was initiated rather than when the paste operation takes place. Once the IDataObject interface is on the Clipboard, you simply wait until someone requests the data. If a supported data format is requested, you copy the data from your STGMEDIUM structure to the STGMEDIUM structure that was passed to you. First you need to declare your COM interfaces for supporting the IDataObject and IEnumFORMATETC interfaces (see Listings 11.16 and 11.17).
Listing 11.16 IDATAOBJECT.H--IDataObject Interface Macro #define DECLARE_STANDARD_DATAOBJECT() \ STDMETHOD(GetData)(LPFORMATETC, LPSTGMEDIUM); \ STDMETHOD(GetDataHere)(LPFORMATETC, LPSTGMEDIUM); \ STDMETHOD(QueryGetData)(LPFORMATETC); \ STDMETHOD(GetCanonicalFormatEtc)(LPFORMATETC, LPFORMATETC); \ STDMETHOD(SetData)(LPFORMATETC, LPSTGMEDIUM, BOOL); \ STDMETHOD(EnumFormatEtc)(DWORD, LPENUMFORMATETC*); \ STDMETHOD(DAdvise)(LPFORMATETC, DWORD, LPADVISESINK, LPDWORD); \ STDMETHOD(DUnadvise)(DWORD); \ STDMETHOD(EnumDAdvise)(LPENUMSTATDATA*);
Listing 11.17 IENUMFORMATETC--IEnumFORMATETC Interface Macro #define DECLARE_STANDARD_ENUMFORMATETC() \ STDMETHOD(Next)(ULONG celt, FORMATETC RPC_FAR * rgelt, \ ULONG_RPC_FAR * pceltFetched); \ STDMETHOD(Skip)(ULONG celt); \ STDMETHOD(Reset)(void); \ STDMETHOD(Clone)(IEnumFORMATETC __RPC_FAR *__RPC_FAR * ppenum); You need to include the header files of your new interface macros, add the IDataObject and IEnumFORMATETC interfaces to your inheritance structure, and add the interface macros to your
control declaration. You also need to add some functions and member variables to aid in your Clipboard support implementation (see Listing 11.18). You use the functions CopyStgMedium, CopyDataToClipboard, and PrepareDataForTransfer to prepare your data structures--the member variables sTextFormatEtc and sTextStgMedium-- for a potential paste operation. The member variable ulFORMATETCElement is the internal counter for the FORMATETC enumerator interface, and OnKeyDown is where all of your Clipboard operations are initiated.
Listing 11.18 BCFCONTROLCTL.H--Clipboard Support Implementation--Header File . . . #include "IPerPropertyBrowsing.h" #include "IDataObject.h" #include "IEnumFORMATETC.h" #include "alignmentenums.h" typedef enum { BCFControlEvent_Change = 0, } BCFCONTROLEVENTS; . . . // class CBCFControlControl : public CInternetControl, public IBCFControl, public ISupportErrorInfo, public IPerPropertyBrowsing, public IDataObject, public IEnumFORMATETC { public: . . . // ISupportErrorInfo methods // DECLARE_STANDARD_SUPPORTERRORINFO(); // IPerPropertyBrowsing methods // DECLARE_STANDARD_PERPROPERTYBROWSING(); // IDataObject methods // DECLARE_STANDARD_DATAOBJECT(); // IEnumFORMATETC methods
// DECLARE_STANDARD_ENUMFORMATETC(); // IBCFControl methods // STDMETHOD(get_Alignment)(THIS_ long FAR* lRetValue); STDMETHOD(put_Alignment)(THIS_ long lNewValue); STDMETHOD(get_BackColor)(THIS_ OLE_COLOR FAR* ocRetValue); . . . void GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy); BOOL bRetrievedDimensions; int iCharWidthArray[256]; int iCharacterSpacing, iCharacterHeight; void CopyStgMedium(LPSTGMEDIUM lpTargetStgMedium, LPSTGMEDIUM lpSourceStgMedium, CLIPFORMAT cfSourceFormat); void CopyDataToClipboard(void); void PrepareDataForTransfer(void); ULONG ulFORMATETCElement; void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); private: FORMATETC sTextFormatEtc; STGMEDIUM sTextStgMedium; }; You need to update the constructor to initialize your enumerator to the beginning of the enumeration (see Listing 11.19).
Listing 11.19 BCFCONTROLCTL.CPP--Constructor Member Initialization . . .
hOldBrush = hBrush = NULL; // clear the flag bRetrievedDimensions = FALSE; // set to the first element ulFORMATETCElement = 0; // clear the storage medium sTextStgMedium.hGlobal = NULL; } #pragma warning(default:4355) // using `this' in constructor Since you have added two additional COM interfaces to your control, you also need to update your QueryInterface implementation (see Listing 11.20).
Listing 11.20 BCFCONTROLCTL.CPP--QueryInterface Update HRESULT CBCFControlControl::InternalQueryInterface(REFIID riid, void **ppvObjOut) { IUnknown *pUnk; *ppvObjOut = NULL; // TODO: if you want to support any additional interfaces, then you should // indicate that here. never forget to call COleControl's version in the // case where you don't support the given interface. // if(DO_GUIDS_MATCH(riid, IID_IBCFControl)) pUnk = (IUnknown *)(IBCFControl *)this; else if(DO_GUIDS_MATCH(riid, IID_IPerPropertyBrowsing)) pUnk = (IUnknown *)(IPerPropertyBrowsing *)this; else if(DO_GUIDS_MATCH(riid, IID_IDataObject)) pUnk = (IUnknown *)(IDataObject *)this;
else if(DO_GUIDS_MATCH(riid, IID_IEnumFORMATETC)) pUnk = (IUnknown *)(IEnumFORMATETC *)this; else return COleControl::InternalQueryInterface(riid, ppvObjOut); pUnk->AddRef(); *ppvObjOut = (void *)pUnk; return S_OK; } You also need to update your WindowProc function to look for the WM_KEYDOWN message so that you can process the keystrokes that will initiate your Clipboard data transfer (see Listing 11.21).
NOTE: Listing 11.21 contains a switch statement that is used to route Windows messages to the proper message handler. The default handler will call the method OcxDefWindowProc. Whenever you want to use the default implementation for a message, you call OcxDefWindowProc. OcxDefWindowProc is designed to deal with the cases when the control does not have a window handle, because the control may have been created as windowless. Remember that the control will not have its own window handle when it is created windowless, so you should never use the handle directly. Always allow the default BaseCtl implementation to handle the windowless processing of messages.
Listing 11.21 BCFCONTROLCTL.CPP--WM_KEYDOWN Message Handler LRESULT CBCFControlControl::WindowProc ( UINT msg, WPARAM wParam, LPARAM lParam ) {
// TODO: handle any messages here, like in a normal window // proc. note that for special keys, you'll want to override and // implement OnSpecialKey. // if you're a windowed OCX, you should be able to use any of the // win32 API routines except for SetFocus. you should always use // OcxSetFocus() // LRESULT lRetVal = FALSE; switch(msg) { case WM_KEYDOWN: this->OnKeyDown(wParam, LOWORD(lParam), HIWORD(lParam)); break; default: lRetVal = OcxDefWindowProc(msg, wParam, lParam); break; } return lRetVal; } Finally you need to add all of the code for the methods that you declared in your header file. Take a look at all of the methods in detail. The CopyDataToClipboard is function called to initiate a Clipboard transfer (see Listing 11.22). You first check to see whether you are the owner of the Clipboard and set the Boolean variable accordingly. You then prepare your data for the Clipboard, and if you are not the owner of the Clipboard, you flush the data on it and set your IDataObject reference on the Clipboard.
Listing 11.22 BCFCONTROLCTL.CPP--CopyDataToClipboard Implementation void CBCFControlControl::CopyDataToClipboard(void) {
BOOL bHaveClipboard = TRUE; // if we don't have an IDataObject on the clipboard? if(::OleIsCurrentClipboard((IDataObject *) this) != S_OK) // set the flag bHaveClipboard = FALSE; // put data in the storage this->PrepareDataForTransfer(); // if we don't have the clipboard if(!bHaveClipboard) { // clear the clipboard ::OleFlushClipboard(); // put the data on the clipboard ::OleSetClipboard((IDataObject *) this); } } PrepareDataForTransfer (see Listing 11.23) is the function you call when you want to copy the data from your control to the STGMEDIUM structure that will represent your data on the Clipboard. First you allocate a block of global memory that will contain your caption in ANSI format. Then you set up your FORMATETC and STGMEDIUM structures to reflect the correct data type.
Listing 11.23 BCFCONTROLCTL.CPP--PrepareDataForTransfer Implementation void CBCFControlControl::PrepareDataForTransfer(void) { // get the length of the data to copy long lLength = lstrlen(m_lptstrCaption) + 1; // create a global memory object HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sizeof(TCHAR) * lLength);
// lock the memory down LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(hGlobal); // copy the string for(long lCount = 0; lCount < lLength - 1; lCount++) lpTempBuffer[lCount] = m_lptstrCaption[lCount]; // null terminate the string lpTempBuffer[lCount] = `\0'; // unlock the memory ::GlobalUnlock(hGlobal); // copy all of the members sTextFormatEtc.cfFormat = CF_TEXT; sTextFormatEtc.ptd = NULL; sTextFormatEtc.dwAspect = 0; sTextFormatEtc.lindex = -1; sTextFormatEtc.tymed = TYMED_HGLOBAL; // if we have already allocated the data if(sTextStgMedium.hGlobal) // release it ::ReleaseStgMedium(&sTextStgMedium); sTextStgMedium.tymed = TYMED_HGLOBAL; sTextStgMedium.hGlobal = hGlobal; sTextStgMedium.pUnkForRelease = NULL; } CopyStgMedium (see Listing 11.24) is a simple helper function to copy one STGMEDIUM structure to another. The function relies on the OleDuplicateData function to create a new copy of the global memory stored in the source STGMEDIUM. The copied data is then stored in the target STGMEDIUM structure.
Listing 11.24 BCFCONTROLCTL.CPP--CopyStgMedium Implementation
void CBCFControlControl::CopyStgMedium(LPSTGMEDIUM lpTargetStgMedium, LPSTGMEDIUM lpSourceStgMedium, CLIPFORMAT cfSourceFormat) { // copy the stgmedium members lpTargetStgMedium->tymed = lpSourceStgMedium->tymed; lpTargetStgMedium->pUnkForRelease = lpSourceStgMedium>pUnkForRelease; lpTargetStgMedium->hGlobal = ::OleDuplicateData(lpSourceStgMedium>hGlobal, cfSourceFormat, GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT); } The next set of functions (see Listing 11.25) are implemented for the IDataObject interface that you declared in your header file. A number of methods are not implemented and return the value E_NOTIMPL because they are not needed for this implementation. GetData is the function called when you need to copy the data in your STGMEDIUM structure to the STGMEDIUM structure that is supplied. You first see whether the format that is requested matches the data that you support and, if so, copy the data using your helper function. EnumFormatEtc is the method called when the requesting application wants to enumerate your supported formats. You support only the DATADIR_GET direction, which means you can support only the GetData function and not the SetData function of the IDataObject interface. The remainder of the functions are not implemented and simply return the constant E_NOTIMPL.
Listing 11.25 BCFCONTROLCTL.CPP--IDataObject Implementation STDMETHODIMP CBCFControlControl::GetData(LPFORMATETC lpFormatEtc, LPSTGMEDIUM lpStgMedium) { // if this is a format that we can deal with if(lpFormatEtc->cfFormat == CF_TEXT && lpFormatEtc->tymed & TYMED_HGLOBAL)
{ // get a copy of the current stgmedium this->CopyStgMedium(lpStgMedium, &sTextStgMedium, CF_TEXT); return S_OK; } else return DATA_E_FORMATETC; } STDMETHODIMP CBCFControlControl::GetDataHere(LPFORMATETC /*lpFormatEtc*/, LPSTGMEDIUM /*lpStgMedium*/) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::QueryGetData(LPFORMATETC /*lpFormatEtc*/) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::GetCanonicalFormatEtc(LPFORMATETC /*lpFormatEtcIn*/, LPFORMATETC /*lpFormatEtcOut*/) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::SetData(LPFORMATETC /*lpFormatEtc*/, LPSTGMEDIUM /*lpStgMedium*/, BOOL /*bRelease*/) {
return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::EnumFormatEtc(DWORD dwDirection, LPENUMFORMATETC * ppenumFormatEtc) { // we support "get" operations if(dwDirection == DATADIR_GET) { // make the assignment *ppenumFormatEtc = (IEnumFORMATETC *) this;
// increment the reference count (*ppenumFormatEtc)->AddRef(); // return success return S_OK; } return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::DAdvise(FORMATETC * /*pFormatEtc*/, DWORD /*advf*/, LPADVISESINK /*pAdvSink*/, DWORD * /*pdwConnection*/) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::DUnadvise(DWORD /*dwConnection*/) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::EnumDAdvise(LPENUMSTATDATA *
/*ppenumAdvise*/) { return E_NOTIMPL; } The next set of functions (see Listing 11.26) are implemented for the IEnumFORMATETC interface that you declared in your header file. Cloning is not supported and will return the value E_NOTIMPL. The Next function is used to enumerate through the entire list of supported formats. You first check to see whether your counter is set to the first element and that the user asked for at least one entry. If so, you set the FORMATETC structure to your supported format, and if appropriate, you set the number of elements that you are returning and increment the counter. The Skip function advances the enumerator by the number of elements specified. The Reset function sets the enumerator back to the beginning of the enumeration.
Listing 11.26 BCFCONTROLCTL.CPP--IEnumFORMATETC Implementation STDMETHODIMP CBCFControlControl::Next(ULONG celt, FORMATETC_RPC_FAR * rgelt, ULONG RPC_FAR * pceltFetched) { // if we are at the beginning of the enumeration if(ulFORMATETCElement == 0 && celt > 0) { // copy all of the members rgelt->cfFormat = CF_TEXT; rgelt->ptd = NULL; rgelt->dwAspect = 0; rgelt->lindex = -1; rgelt->tymed = TYMED_HGLOBAL;
// if the caller wants to know how many we copied if(pceltFetched) *pceltFetched = 1; // increment the counter ulFORMATETCElement++; // return success return S_OK; } else // return failure return S_FALSE; } STDMETHODIMP CBCFControlControl::Skip(ULONG celt) { // move the counter by the number of elements supplied ulFORMATETCElement += celt;
// return success return S_OK; } STDMETHODIMP CBCFControlControl::Reset(void) { // reset to the beginning of the enumerator ulFORMATETCElement = 0;
// return success return S_OK;
} STDMETHODIMP CBCFControlControl::Clone(IEnumFORMATETC RPC_FAR *__RPC_FAR * /*ppenum*/) { return E_NOTIMPL; } Finally you are at the end of your implementation: the OnKeyDown function (see Listing 11.27). The OnKeyDown contains all of the code that is necessary to look for the common keystroke combinations used to initiate Clipboard operations.
Listing 11.27 BCFCONTROLCTL.CPP--OnKeyDown Implementation void CBCFControlControl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { BOOL bHandled = FALSE; // find out if the shift key is being held down short sShift = ::GetKeyState(VK_SHIFT); // find out if the control key is being held down short sControl = ::GetKeyState(VK_CONTROL); switch(nChar) { // COPY or PASTE case 0x43: // `C' case 0x63: // `c' // if the control key is being held down if(sControl & 0x8000) { // copy the data to the clipboard
this->CopyDataToClipboard(); // we don't need to pass this key to the base implementation bHandled = TRUE; } break; case 0x58: // `X' case 0x78: // `x' case VK_DELETE: // if this is a shift delete OR CTRL-X/x if((nChar == VK_DELETE && (sShift & 0x8000)) || ((nChar == 0x58 || nChar == 0x78) && (sControl & 0x8000))) { this->CopyDataToClipboard(); // clear the string since this is a CUT operation delete [] m_lptstrCaption; // NULL terminate the string reference m_lptstrCaption = new TCHAR[1]; m_lptstrCaption[0] = `\0'; // fire the global change event this->FireChange(); // force the control to repaint itself this->InvalidateControl(NULL); // we don't need to pass this key to the base implementation bHandled = TRUE; } break; } // if we didn't handle the character if(!bHandled)
{ // and the control key is not being held down if(!(sControl & 0x8000)) // send to the default handler this->OcxDefWindowProc(WM_KEYDOWN, (WPARAM) nFlags, MAKELPARAM(nRepCnt, nFlags)); } } Now that you know how to put data on the Clipboard, take a look at how you get data off the Clipboard. Enabling a Control as a Clipboard Target The opposite of being a Clipboard source is being a Clipboard target. The first step in enabling your control as a Clipboard target is to update your header file with two additional member functions: GetDataFromClipboard and GetDataFromTransfer (see Listing 11.28).
Listing 11.28 BCFCONTROCTL.H--Clipboard Target Implementation-- Header File . . . void CopyDataToClipboard(void); void PrepareDataForTransfer(void); void GetDataFromClipboard(void); void GetDataFromTransfer(IDataObject * ipDataObj); ULONG ulFORMATETCElement; void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); private: FORMATETC sTextFormatEtc; . . . The next step is to update your source file with the GetDataFromClipboard and GetDataFromTransfer implementations (see Listing 11.29). The first method, GetDataFromClipboard, which, as the name implies, gets the data from the Clipboard and
transfers it to your control. GetDataFromClipboard first checks the Clipboard to see whether you already own it. If you do, you refresh the control's data with the data that is stored in the STGMEDIUM structure. You do this because the data stored in the control may have changed since the data was originally pasted to the Clipboard. If you don't already own the Clipboard, you get the IDataObject reference of the object that does and pass it on to your GetDataFromTransfer function.
Listing 11.29 BCFCONTROLCTL.CPP--GetDataFromClipboard Implementation void CBCFControlControl::GetDataFromClipboard(void) { // get an IDataObject pointer IDataObject * ipClipboardDataObj = NULL; // do we have an IDataObject on the clipboard? if(::OleIsCurrentClipboard((IDataObject *) this) == S_OK) { // get the global data for this format and lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sTextStgMedium.hGlobal); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpTempBuffer); // unlock the memory
::GlobalUnlock(sTextStgMedium.hGlobal); return; } else if(::OleGetClipboard(&ipClipboardDataObj) == S_OK) { // transfer the data to the control this->GetDataFromTransfer(ipClipboardDataObj); // release the IDataObject ipClipboardDataObj->Release(); } } GetDataFromTransfer requests the IEnumFORMATETC interface from the IDataObject and cycles through all of the supported formats looking for one that matches yours (see Listing 11.30). Upon finding the appropriate format, it requests the data from the IDataObject supplying a FORMATETC and STGMEDIUM structure. The data is transferred to the control, and the STGMEDUIM is released. The next thing you do is release your interface pointers. The last step, if you find a format, is to force the control to repaint itself reflecting the new state of the control.
Listing 11.30 BCFCONTROLCTL.CPP--GetDataFromTransfer Implementation void CBCFControlControl::GetDataFromTransfer(IDataObject * ipDataObj) { IEnumFORMATETC * ipenumFormatetc; BOOL bFound = FALSE; // get a FORMATETC enumerator if(ipDataObj->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc) == S_OK) { // reset the enumerator just to be safe ipenumFormatetc->Reset(); FORMATETC etc; // while there are formats to enumerate
while(ipenumFormatetc->Next(1, &etc, NULL) == S_OK && !bFound) { // is this a format that we are looking for? if(etc.cfFormat == CF_TEXT && etc.tymed & TYMED_HGLOBAL) { STGMEDIUM sStgMediumData; // get the data from the stgmedium if(ipDataObj->GetData(&etc, &sStgMediumData) == S_OK) { // get the global data for this format and lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sStgMediumData.hGlobal); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpTempBuffer); // unlock the memory ::GlobalUnlock(sStgMediumData.hGlobal); // release the storage medium ::ReleaseStgMedium(&sStgMediumData); // terminate the loop
bFound = TRUE; } } }
// release the enumerator ipenumFormatetc->Release(); } // if we found a format if(bFound == TRUE) // force the control to repaint itself this->InvalidateControl(NULL); } Last you add the code that will initiate the transfer; you do this in your OnKeyDown function (see Listing 11.31).
Listing 11.31 BCFCONTROCTL.CPP--OnKeyDown Implementation void CBCFControlControl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { BOOL bHandled = FALSE; // find out if the shift key is being held down short sShift = ::GetKeyState(VK_SHIFT); // find out if the control key is being held down short sControl = ::GetKeyState(VK_CONTROL); switch(nChar) { // PASTE
case 0x56: // `V' case 0x76: // `v' // if the control key is being held down if(sControl & 0x8000) { // get any text from the clipboard this->GetDataFromClipboard(); // force the control to redraw itself this->InvalidateControl(NULL); // we don't need to pass this key to the base implementation bHandled = TRUE; } break; // COPY or PASTE case 0x43: // `C' case 0x63: // `c' case VK_INSERT: // if the control key is being held down if(sControl & 0x8000) { // copy the data to the clipboard this->CopyDataToClipboard(); // we don't need to pass this key to the base implementation bHandled = TRUE; } // if the shift key is being held down it is a PASTE else if(sShift & 0x8000 && nChar == VK_INSERT)
{ // get any text from the clipboard this->GetDataFromClipboard(); // force the control to redraw itself this->InvalidateControl(NULL); // we don't need to pass this key to the base implementation bHandled = TRUE; } break; case 0x58: // `X' case 0x78: // `x' case VK_DELETE: // if this is a shift delete OR CTRL-X/x . . . While not as simple to implement as MFC, Clipboard support in a BaseCtl implementation can be added in a relatively short period of time with very satisfying results. To round out your implementation, you take the next logical step, which is Drag and Drop support.
Adding Drag and Drop Support The fundamentals of Drag and Drop are very similar to Clipboard support. In addition to the Clipboard interfaces, Drag and Drop requires two new interfaces: IDropSource and IDropTarget. IDropSource is for those controls that can create data that can be dropped onto another application. IDropTarget is for those controls that can accept data that has been dropped from another application. Using Built-In Drag and Drop Formats Since Drag and Drop is similar to a Clipboard transfer, it relies on the same built-in data formats as Clipboard transfers. See the section entitled "Using Built-In Clipboard Formats" for more information regarding the types of formats available. Enabling a BaseCtl control implementation for Drag and Drop support is similar in complexity to the work you did to enable Clipboard transfers. Enabling a Control as a Drag and Drop Source The first part of your implementation is to enable your control as a Drag and Drop source. To be a Drag and Drop source, the control must implement the IDropSource interface in addition to the IDataObject and IEnumFORMATETC interfaces.
The IDropSource interface is declared in the same manner as your other COM interfaces (see Listing 11.32). The implementation is fairly simple since the interface consists of only two methods.
Listing 11.32 IDROPSOURCE.H--IDropSource Interface #define DECLARE_STANDARD_DROPSOURCE() \ STDMETHOD(QueryContinueDrag)(BOOL fEscapePressed, DWORD dwKeyState); \ STDMETHOD(GiveFeedback)(DWORD dwEffect); Next you add the include file for the new interface, inherit your control from the interface, and add your interface macro to your control implementation (see Listing 11.33). You also add the prototype for OnLButtonDown, which is the function that initiates your Drag and Drop operation.
Listing 11.33 BCFCONTROCTL.H--IDropSource Interface Implementation . . . #include "IEnumFORMATETC.h" #include "IDropSource.h" #include "alignmentenums.h" . . . class CBCFControlControl : public CInternetControl, public IBCFControl, public ISupportErrorInfo, public IPerPropertyBrowsing, public IDataObject, public IEnumFORMATETC, public IDropSource { public: . . . // IDropSource methods // DECLARE_STANDARD_DROPSOURCE(); // IBCFControl methods // STDMETHOD(get_Alignment)(THIS_ long FAR* lRetValue); STDMETHOD(put_Alignment)(THIS_ long lNewValue);
. . . ULONG ulFORMATETCElement; void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); void OnLButtonDown(UINT nFlags, short sHor, short sVer); private: FORMATETC sTextFormatEtc; STGMEDIUM sTextStgMedium; }; . . . Since you added a new interface, you also need to update your QueryInterface function (see Listing 11.34).
Listing 11.34 BCFCONTROLCTL.CPP--QueryInterface Update HRESULT CBCFControlControl::InternalQueryInterface(REFIID riid, void **ppvObjOut) { IUnknown *pUnk; *ppvObjOut = NULL; // TODO: if you want to support any additional interfaces, then you should // indicate that here. never forget to call COleControl's version in the // case where you don't support the given interface. // if(DO_GUIDS_MATCH(riid, IID_IBCFControl)) pUnk = (IUnknown *)(IBCFControl *)this; else if(DO_GUIDS_MATCH(riid, IID_IPerPropertyBrowsing)) pUnk = (IUnknown *)(IPerPropertyBrowsing *)this; else if(DO_GUIDS_MATCH(riid, IID_IDataObject)) pUnk = (IUnknown *)(IDataObject *)this; else if(DO_GUIDS_MATCH(riid, IID_IEnumFORMATETC))
pUnk = (IUnknown *)(IEnumFORMATETC *)this; else if(DO_GUIDS_MATCH(riid, IID_IDropSource)) pUnk = (IUnknown *)(IDropSource *)this; else return COleControl::InternalQueryInterface(riid, ppvObjOut); pUnk->AddRef(); *ppvObjOut = (void *)pUnk; return S_OK; } You have to update your WindowProc function to route the WM_LBUTTONDOWN messages to your OnLButtonDown function (see Listing 11.35).
Listing 11.35 BCFCONTROLCTL.CPP--WindowProc Implementation LRESULT CBCFControlControl::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) { LRESULT lRetVal = FALSE; switch(msg) { case WM_KEYDOWN: this->OnKeyDown(wParam, LOWORD(lParam), HIWORD(lParam)); break; case WM_LBUTTONDOWN: this->OnLButtonDown(wParam, (short) LOWORD(lParam), (short) HIWORD(lParam)); this->OcxDefWindowProc(msg, wParam, lParam); break;
default: lRetVal = this->OcxDefWindowProc(msg, wParam, lParam); break; } return lRetVal; } Next you need to implement your OnLButtonDown function and the two interface functions for your IDropSource interface (see Listing 11.36). When OnLButtonDown is called, the control calls your PrepareDataForTransfer function to set up the data in your IDataObject interface. You then call DoDragDrop supplying the IDataObject and IDropSource interfaces and the drop effect that you want to display. See the VC++ documentation for more information about the IDataObject and IDataSource interfaces and the types of drop effects that you have at your disposal. QueryContinueDrag is used to instruct OLE as to how the Drag and Drop operation should be handled at the time the state of the keyboard or mouse changes. When the keyboard or mouse state changes, it is usually indicative of a drop operation. In your case, you look to see whether the left mouse button is no longer being held down. If that is the case, the drop operation is completed. Otherwise, you just exit the method. GiveFeedback is used to instruct OLE as to what cursors should be used while performing the Drag and Drop operation. In your case, you use the default cursors. See the VC++ documentation for more information on how to support different cursors.
Listing 11.36 BCFCONTROLCTL.CPP--Drop Source Implementation void CBCFControlControl::OnLButtonDown(UINT nFlags, short sHor, short sVer) { // call the common data preparation function this->PrepareDataForTransfer(); DWORD dwDropEffect = DROPEFFECT_NONE;
// start the Drag and Drop operation ::DoDragDrop((IDataObject *) this, (IDropSource *) this,
DROPEFFECT_COPY, &dwDropEffect); } STDMETHODIMP CBCFControlControl::QueryContinueDrag(BOOL fEscapePressed, DWORD dwKeyState) { // if the left button has been released if(!(dwKeyState & MK_LBUTTON)) // it is OK to drop return DRAGDROP_S_DROP; else // return success return S_OK; } STDMETHODIMP CBCFControlControl::GiveFeedback(DWORD dwEffect) { // use the default cursors return DRAGDROP_S_USEDEFAULTCURSORS; } As you can see, very little code is required to be a Drag and Drop source, but being a Drag and Drop source is only half the battle. To develop a truly complete implementation, you need to include support as a Drag and Drop target. Enabling a Control as a Drag and Drop Target As with your Drag and Drop source support, you build upon the interfaces you've already created and add some new functionality. To be a Drag and Drop target, a control must implement the IDropTarget interface (see Listing 11.37).
Listing 11.37 IDROPTARGET.H--IDropTarget Interface #define DECLARE_STANDARD_IDROPTARGET() \
STDMETHOD(DragEnter)(LPDATAOBJECT pDataObject, DWORD dwKeyState, POINTL pt, LPDWORD pdwEffect); \ STDMETHOD(DragOver)(DWORD dwKeyState, POINTL pt, LPDWORD pdwEffect); \ STDMETHOD(DragLeave)(void); \ STDMETHOD(Drop)(LPDATAOBJECT pDataObject, DWORD dwKeyState, POINTL pt, \ LPDWORD pdwEffect); Now that you have your interface macro declared, you need to add the interface to your control implementation. To do this, you need to add your macro include file, inherit from the COM interface, and add the IDropTarget macro to your control (see Listing 11.38). You also add the AfterCreateWindow function, which is defined in the COleControl base class. AfterCreateWindow is where you register your control as a valid Drag and Drop target.
Listing 11.38 BCFCONTROLCTL.H--IDropTarget Implementation . . . #include "IDataObject.h" #include "IEnumFORMATETC.h" #include "IDropSource.h" #include "IDropTarget.h" #include "alignmentenums.h" . . . class CBCFControlControl : public CInternetControl, public IBCFControl, public ISupportErrorInfo, public IPerPropertyBrowsing, public IDataObject, public IEnumFORMATETC, public IDropSource, public IDropTarget { public: . . . // IDropTarget methods //
DECLARE_STANDARD_IDROPTARGET(); // IBCFControl methods // . . . virtual HRESULT InternalQueryInterface(REFIID, void **); virtual BOOL BeforeCreateWindow(DWORD *pdwWindowStyle, DWORD *pdwExWindowStyle, LPSTR pszWindowTitle); virtual void BeforeDestroyWindow(void); virtual BOOL AfterCreateWindow(void); /// OnData is called asynchronously as data for an object or property arrives... virtual HRESULT OnData(DISPID propId, DWORD bscfFlag, IStream * strm, DWORD dwSize); // private state information. // . . . Since you have a COM interface, you also need to update your QueryInterface function (see Listing 11.39).
Listing 11.39 BCFCONTROLCTL.CPP--QueryInterface Update
HRESULT CBCFControlControl::InternalQueryInterface(REFIID riid, void **ppvObjOut) { IUnknown *pUnk; *ppvObjOut = NULL; // TODO: if you want to support any additional interfaces, then you should // indicate that here. never forget to call COleControl's version in the // case where you don't support the given interface. //
if(DO_GUIDS_MATCH(riid, IID_IBCFControl)) pUnk = (IUnknown *)(IBCFControl *)this; else if(DO_GUIDS_MATCH(riid, IID_IPerPropertyBrowsing)) pUnk = (IUnknown *)(IPerPropertyBrowsing *)this; else if(DO_GUIDS_MATCH(riid, IID_IDataObject)) pUnk = (IUnknown *)(IDataObject *)this; else if(DO_GUIDS_MATCH(riid, IID_IEnumFORMATETC)) pUnk = (IUnknown *)(IEnumFORMATETC *)this; else if(DO_GUIDS_MATCH(riid, IID_IDropSource)) pUnk = (IUnknown *)(IDropSource *)this; else if(DO_GUIDS_MATCH(riid, IID_IDropTarget)) pUnk = (IUnknown *)(IDropTarget *)this; else return COleControl::InternalQueryInterface(riid, ppvObjOut); pUnk->AddRef(); *ppvObjOut = (void *)pUnk; return S_OK; } In order for your control to be a Drop Target, it must do more than just support the appropriate interfaces. You must register your control as a Drop Target. You do this in the AfterCreateWindow function you added earlier. Listing 11.40 shows your implementation of your Register and, correspondingly, your Revoke implementation. For this reason alone, your control must have a window. Controls that support Windowless Activation can still be Drag and Drop targets, but they must create a window when a Drag and Drop operation occurs. See the VC++ documentation for more information on supporting Drag and Drop operations in windowless controls.
Listing 11.40 BCFCONTROLCTL.CPP--AfterCreateWindow Implementation BOOL CBCFControlControl::AfterCreateWindow(void) {
// if we have a window handle if(m_hwnd) // register the control as a drag and drop target ::RegisterDragDrop(m_hwnd, (IDropTarget *) this); return TRUE; } void CBCFControlControl::BeforeDestroyWindow(void) { // if we have a window handle if(m_hwnd) // revoke the control as a drag and drop target ::RevokeDragDrop(m_hwnd); // if there is an old brush if(hOldBrush) { // get the DC HDC hDC = this->OcxGetDC(); // select the old brush back ::SelectObject(hDC, hOldBrush); // release the DC this->OcxReleaseDC(hDC); } // if we created a brush if(hBrush) // destroy the brush we created ::DeleteObject(hBrush); } Last you implement your IDropTarget interface (see Listing 11.41).
DragEnter is where you instruct OLE as to whether the current drag operation that has entered your control is valid for your implementation. You first look for the appropriate mouse or keyboard state, which, in your case, is the left mouse button being held down. Next you use the IEnumFORMATETC interface to see if the IDataObject that was passed to you contains formats that you can use. DragOver is used to instruct Windows as to the current state that the drag operation is in while it is over your control. Your implementation is very easy. One could, however, implement the method to allow the Drag and Drop operation over only specific portions of the control by checking the point structure that was passed in and comparing it to various locations of the control. For example, a grid control might allow only text data to be dropped on the headings but allow text and numeric data to be dropped over the columns. DragLeave is used to clean up any state information that may have been created locally to the control when the DragEnter was invoked. In your case, you return E_NOTIMPL since you have no use for the function. Drop is the last function that you need to implement and is where you copy the data from the IDataObject to your control using the GetDataFromTransfer method.
Listing 11.41 BCFCONTROLCTL.CPP--IDropTarget Implementation STDMETHODIMP CBCFControlControl::DragEnter(LPDATAOBJECT pDataObject, DWORD dwKeyState, POINTL pt, LPDWORD pdwEffect) { // if the left mouse button is being held down if(dwKeyState & MK_LBUTTON) { IEnumFORMATETC * ipenumFormatetc; BOOL bFound = FALSE; // get a FORMATETC enumerator if(pDataObject->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc) == S_OK) { // reset the enumerator just to be safe ipenumFormatetc->Reset();
FORMATETC etc; // while there are formats to enumerate while(ipenumFormatetc->Next(1, &etc, NULL) == S_OK && !bFound) { // is this a format that we are looking for? if(etc.cfFormat == CF_TEXT && etc.tymed & TYMED_HGLOBAL) bFound = TRUE; } // release the enumerator ipenumFormatetc->Release(); } // is there a text format available if(bFound) *pdwEffect = DROPEFFECT_COPY; // everything else we can't deal with else *pdwEffect = DROPEFFECT_NONE; } else // not the left mouse *pdwEffect = DROPEFFECT_NONE; // return success return S_OK; } STDMETHODIMP CBCFControlControl::DragOver(DWORD dwKeyState, POINTL pt, LPDWORD pdwEffect) {
// if the left mouse button is being held down if(dwKeyState & MK_LBUTTON) // copy *pdwEffect = DROPEFFECT_COPY; else // not the left mouse *pdwEffect = DROPEFFECT_NONE; // return success return S_OK; } STDMETHODIMP CBCFControlControl::DragLeave(void) { return E_NOTIMPL; } STDMETHODIMP CBCFControlControl::Drop(LPDATAOBJECT pDataObject, DWORD dwKeyState, POINTL pt, LPDWORD pdwEffect) { // transfer the data to the control this->GetDataFromTransfer(pDataObject); // return success return S_OK; } As with your MFC and ATL implementations, adding Drag and Drop support is straightforward. Now that you have addressed the built-in formats, take a look at the next step, custom formats.
Custom Clipboard and Drag and Drop Formats A custom data format is one that is understood by the exchanging applications but does not fall into the category of predefined formats. For your implementation, you transfer the text Alignment property along with your Caption. You are not restricted in any way in the types of data that can be
transferred in this manner. Adding custom data formats is independent of the mechanism used to initiate the data transfer. Since you have modeled your data transfer methods based on this principle, you need to make only one set of changes to your application to accommodate both Clipboard and Drag and Drop operations. The first step is adding the member variables that you will use to implement your custom format (see Listing 11.42). m_uiCustomFormat is used to hold the ID number of the registered custom format. The remaining members are used to hold the data and its related formatting information.
Listing 11.42 BCFCONTROLCTL.H--Custom Data Format Member Variables . . . private: FORMATETC sTextFormatEtc; STGMEDIUM sTextStgMedium; // custom format storage variables UINT m_uiCustomFormat; FORMATETC sCustomFormatEtc; STGMEDIUM sCustomStgMedium; }; The next step is to initialize your member variables to valid values, which you do in your constructor (see Listing 11.43). When you register the format, you are actually registering the format in the Windows OS. That way, whenever an application needs to use the format, it will get the same value as that of the application that registered the format in the first place. All applications that need to use a custom format must call this method to retrieve the ID associated with the custom format type.
Listing 11.43 BCFCONTROLCTL.CPP--Register the Custom Format . . . // set to the first element ulFORMATETCElement = 0; // clear the storage medium sTextStgMedium.hGlobal = NULL; // register a custom clipboard format
m_uiCustomFormat = ::RegisterClipboardFormat("BCFControlCtlCustomFormat"); // clear the storage medium sCustomStgMedium.hGlobal = NULL; } #pragma warning(default:4355) // using `this' in constructor Next you update your PrepareDataForTransfer function (see Listing 11.44). In addition to the CF_TEXT format, you add the creation of your custom data format, if there is one. You store the new format in your custom storage variables so that you can support the formats on a granular basis. If the application receiving the data understands only your text format, that is all that it needs to retrieve.
Listing 11.44 BCFCONTROCTL.CPP--PrepareDataForTransfer Update void CBCFControlControl::PrepareDataForTransfer(void) { . . . // if we have custom clipboard format support if(m_uiCustomFormat) { // create a global memory object HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sizeof(m_state.lAlignment));
// lock the memory down LONG * lpTempBuffer = (LONG *) ::GlobalLock(hGlobal); // set our data buffer *lpTempBuffer = m_state.lAlignment; // unlock the memory ::GlobalUnlock(hGlobal); // copy all of the members sCustomFormatEtc.cfFormat = m_uiCustomFormat;
sCustomFormatEtc.ptd = NULL; sCustomFormatEtc.dwAspect = 0; sCustomFormatEtc.lindex = -1; sCustomFormatEtc.tymed = TYMED_HGLOBAL; // if we have already allocated the data if(sCustomStgMedium.hGlobal) // release it ::ReleaseStgMedium(&sCustomStgMedium); sCustomStgMedium.tymed = TYMED_HGLOBAL; sCustomStgMedium.hGlobal = hGlobal; sCustomStgMedium.pUnkForRelease = NULL; } } Next you update the GetDataFromTransfer method, which you will use to copy the data from a SGTMEDUIM structure to your control (see Listing 11.45). As with your PrepareDataForTransfer method, you take a granular approach and support the basic text transfer independent of your custom format. Note that you change your while..loop slightly to look through all of the available formats and stop only when you have looked at them all. This way, you can support the text format and the custom format independent of each other and ensure that they are not mutually exclusive.
Listing 11.45 BCFCONTROLCTL.CPP--GetDataFromTransfer Update void CBCFControlControl::GetDataFromTransfer(IDataObject * ipDataObj) { IEnumFORMATETC * ipenumFormatetc; BOOL bFound = FALSE; // get a FORMATETC enumerator if(ipDataObj->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc) == S_OK) {
// reset the enumerator just to be safe ipenumFormatetc->Reset(); FORMATETC etc; // while there are formats to enumerate while(ipenumFormatetc->Next(1, &etc, NULL) == S_OK) { // is this a format that we are looking for? if(etc.cfFormat == CF_TEXT && etc.tymed & TYMED_HGLOBAL) { STGMEDIUM sStgMediumData; // get the data from the stgmedium if(ipDataObj->GetData(&etc, &sStgMediumData) == S_OK) { // get the global data for this format and lock down the memory LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sStgMediumData.hGlobal); // if we have a string if(m_lptstrCaption) { // delete the existing string delete [] m_lptstrCaption; // clear the reference just to be safe m_lptstrCaption = NULL; } // allocate a new string m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer) + 1]; // assign the string to our member variable lstrcpy(m_lptstrCaption, lpTempBuffer); // unlock the memory ::GlobalUnlock(sStgMediumData.hGlobal); // release the storage medium
::ReleaseStgMedium(&sStgMediumData); // indicate success bFound = TRUE; } } // is this a format that we are looking for? else if(m_uiCustomFormat && etc.cfFormat == m_uiCustomFormat && etc.tymed & TYMED_HGLOBAL) { STGMEDIUM sStgMediumData; // get the data from the stgmedium if(ipDataObj->GetData(&etc, &sStgMediumData) == S_OK) { // get the global data for this format and lock down the memory LONG * lpTempBuffer = (LONG *) ::GlobalLock(sStgMediumData.hGlobal); // get the data m_state.lAlignment = *lpTempBuffer; // unlock the memory ::GlobalUnlock(sStgMediumData.hGlobal); // release the storage medium ::ReleaseStgMedium(&sStgMediumData); // indicate success bFound = TRUE; } } }
// release the enumerator
ipenumFormatetc->Release(); } // if we found a format if(bFound == TRUE) // force the control to repaint itself this->InvalidateControl(NULL); } Now that you have updated your basic data transfer routines, you need to update your IEnumFORMATETC interface to essentially publish the availability of the new format to any application that wants it. You do this in your IEnumFORMATETC::Next function (see Listing 11.46). If you support a custom format and are at the second format in your enumerator, you fill in the FORMATETC structure that was passed in with the appropriate information. Doing this will let any application that understands your custom format know that you can also support the custom format. Note that the implementation will return only a single format, even if the caller asked for more than one. You can add code to deal with the cases where more than one format is requested, but adding the additional code doesn't add anything to the sample, so that topic is not addressed here.
Listing 11.46 BCFCONTROLCTL.CPP--IEnumFORMATETC::Next Update STDMETHODIMP CBCFControlControl::Next(ULONG celt, FORMATETC RPC_FAR * rgelt, ULONG RPC_FAR * pceltFetched) { // if we are at the beginning of the enumeration if(ulFORMATETCElement == 0 && celt > 0) { // copy all of the members rgelt->cfFormat = CF_TEXT; rgelt->ptd = NULL; rgelt->dwAspect = 0; rgelt->lindex = -1;
rgelt->tymed = TYMED_HGLOBAL;
// if the caller wants to know how many we copied if(pceltFetched) *pceltFetched = 1; // increment the counter ulFORMATETCElement++; // return success return S_OK; } else if(m_uiCustomFormat && ulFORMATETCElement == 1 && celt > 0) { // copy all of the members rgelt->cfFormat = m_uiCustomFormat; rgelt->ptd = NULL; rgelt->dwAspect = 0; rgelt->lindex = -1; rgelt->tymed = TYMED_HGLOBAL;
// if the caller wants to know how many we copied if(pceltFetched) *pceltFetched = 1; // increment the counter ulFORMATETCElement++; // return success return S_OK; } else
// return failure return S_FALSE; } Last you need to update the routine that returns the custom format in the STGMEDIUM structure, IEnumFORMATETC::GetData (see Listing 11.47). You can still use the CopyStgMedium function; the only difference is which internal STGMEDIUM structure is supplied to the function.
Listing 11.47 BCFCONTROLCTL.CPP--IEnumFORMATETC::GetData Update STDMETHODIMP CBCFControlControl::GetData(LPFORMATETC lpFormatEtc, LPSTGMEDIUM lpStgMedium) { // if this is a format that we can deal with if(lpFormatEtc->cfFormat == CF_TEXT && lpFormatEtc->tymed & TYMED_HGLOBAL) { // get a copy of the current stgmedium this->CopyStgMedium(lpStgMedium, &sTextStgMedium, CF_TEXT); return S_OK; } else if(m_uiCustomFormat && lpFormatEtc->cfFormat == m_uiCustomFormat && lpFormatEtc->tymed & TYMED_HGLOBAL) { // get a copy of the current stgmedium this->CopyStgMedium(lpStgMedium, &sCustomStgMedium, m_uiCustomFormat); return S_OK; }
else return DATA_E_FORMATETC; } That is all it takes to support custom formats. By taking a "black box" approach to creating your data transfer routines (meaning that you create routines that manipulate basic data structures and remove the specific data transfer details from your code), you can support a large amount of functionality relying on a common code base. Adding Clipboard and Drag and Drop support to your control can improve its overall appearance and integration with other controls and the container in which it resides.
Subclassing Existing Windows Controls As with your MFC and ATL implementations, you can support the subclassing of existing Windows controls with the BaseCtl framework. At the beginning of Chapter 10, you created several controls in your application, one of which subclassed a Windows BUTTON control, CBCFControlSubControl. Take a look at the additional code that is required to support subclassing. Listing 11.48 shows the extent of your implementation. RegisterClassData retrieves the class information for the BUTTON control and uses it for your control. OnDraw delegates the painting of the control to the DoSuperClassPaint function. WindowProc delegates the standard windows message handling to the subclassed control.
Listing 11.48 BCFCONTROLSUBCTL.CPP--RegisterClassData Implementation //=------------------------------------------------------------------------= // CBCFControlSubWinControl:RegisterClassData //=------------------------------------------------------------------------= // register the window class information for your control here. // this information will automatically get cleaned up for you on DLL shutdown. //
// Output: // BOOL - FALSE means fatal error. // // Notes: // BOOL CBCFControlSubWinControl::RegisterClassData ( void ) { WNDCLASS wndclass; // subclass a windows BUTTON control. // if (!::GetClassInfo(g_hInstance, "BUTTON", &wndclass)) return FALSE; // this doesn't need a critical section for apartment threading support // since it's already in a critical section in CreateInPlaceWindow // SUBCLASSWNDPROCOFCONTROL(OBJECT_TYPE_CTLBCFCONTROLSUBWIN) = (WNDPROC)wndclass.lpfnWndProc; wndclass.lpfnWndProc = COleControl::ControlWindowProc; wndclass.lpszClassName = WNDCLASSNAMEOFCONTROL(OBJECT_TYPE_CTLBCFCONTROLSUBWIN); return RegisterClass(&wndclass); } . . . //=------------------------------------------------------------------------=
// CBCFControlSubWinControl::OnDraw //=------------------------------------------------------------------------= // "I don't very much enjoy looking at paintings in general. i know too // much about them. i take them apart." // - georgia o'keeffe (1887-1986) // // Parameters: // DWORD - [in] drawing aspect // HDC - [in] HDC to draw to // LPCRECTL - [in] rect we're drawing to // LPCRECTL - [in] window extent and origin for meta-files // HDC - [in] HIC for target device // BOOL - [in] can we optimize DC handling? // // Output: // HRESULT // // Notes: // HRESULT CBCFControlSubWinControl::OnDraw ( DWORD dvAspect, HDC hdcDraw, LPCRECTL prcBounds,
LPCRECTL prcWBounds, HDC hicTargetDevice, BOOL fOptimize ) { // TODO: put your drawing code here ... // return DoSuperClassPaint(hdcDraw, prcBounds); } //=------------------------------------------------------------------------= // CBCFControlSubWinControl::WindowProc //=------------------------------------------------------------------------= // window procedure for this control. nothing terribly exciting. // // Parameters: // see win32sdk on window procs [except HWND -- it's in m_hwnd] // // Notes: // LRESULT CBCFControlSubWinControl::WindowProc ( UINT msg, WPARAM wParam, LPARAM lParam
) { // TODO: handle any messages here, like in a normal window // proc. note that for special keys, you'll want to override and // implement OnSpecialKey. // return CallWindowProc((FARPROC)SUBCLASSWNDPROCOFCONTROL( OBJECT_TYPE_CTLBCFCONTROLSUBWIN), m_hwnd, msg, wParam, lParam); } As you can see, subclassing an existing control is easy. Subclassing can significantly reduce your development effort and presents enormous potential in your ability to create powerful derivations of pre-existing controls.
Dual-Interface Controls BaseCtl control implementations, by default, support dual-interface so no extra work is needed. As we stated in previous chapters, however, currently no control containers can or will use dual-interfaces on controls.
Other ActiveX Features As with your MFC and ATL implementations, the BaseCtl framework allows you to take advantage of some of the available OC 96 or ActiveX features. Chapter 6 contains a detailed explanation of each feature, so you don't go into them here. You will, however, look into their specification implementation aspects. All of the unique information about the control and how it is created is defined in a structure called CONTROLOBJECTINFO. This structure contains the control's name, help file, flags, and so on--all of the required information for the control to be created. This structure is wrapped in four macros that are used when defining a specific type of control (see Table 11.1). Each control implementation must declare one of these macros in its header file to define the control's implementation details. Some of the ActiveX features pointed out in previous chapters are defined in this structure (refer to Chapter 6 for more information). Table 11.1 Windowing Macros Macro
Description
DEFINE_CONTROLOBJECT
This is the standard macro used for declaring a windowed control.
DEFINE_WINDOWLESSCONTROLOBJECT
This is the standard macro used for declaring a windowless control.
DEFINE_CONTROLOBJECT2
This is an extended macro used for declaring a windowed control. It is similar to the standard macro but allows more control over the definition.
DEFINE_WINDOWLESSCONTROLOBJECT2 This is an extended macro used for declaring a windowless control. It is similar to the standard macro but allows more control over the definition.
Windowless Activation Windowless activation is supported through the IOleInPlaceObjectWindowless interface and is implemented in the container. If the container doesn't support windowless activation, the control must be able to create a window for itself. Windowless activation is a request not a guarantee. Listing 11.49 shows your control definition for BCFControlControl, which is your standard windowed ActiveX control.
Listing 11.49 BCFCONTROLCTL.H--BCFControControl ActiveX Implementation // TODO: if you have an array of verbs, then add an extern here with the name // of it, so that you can include it in the DEFINE_CONTROLOBJECT. // ie. extern VERBINFO m_BCFControlCustomVerbs []; // extern const GUID *rgBCFControlPropPages []; DEFINE_CONTROLOBJECT(BCFControl, &CLSID_BCFControl, "BCFControlCtl", CBCFControlControl::Create, 1, &IID_IBCFControl,
"BCFControl.HLP", &DIID_DBCFControlEvents, OLEMISC_SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_RECOMPOSEONRESIZE | OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT, 0, // no IPointerInactive policy by default RESID_TOOLBOX_BITMAP1, "BCFControlWndClass", 1, rgBCFControlPropPages, 0, NULL); Listing 11.50 shows the control definition for BCFControlNoWinControl, a windowless control. The only difference between your windowed and windowless control is this macro. Embedded within the macro is the value indicating that this control is windowed or windowless; you just use the correct macro to get the required behavior. The DEFINE_WINDOWLESSCONTROLOBJECT macro contains one additional parameter: the highlighted TRUE value in Listing 11.50, which is used to define whether the control is 100 percent opaque. In other words, does the control draw over its entire client area, or are there some transparent parts?
Listing 11.50 BCFCONTROLNOWIN.H--BCFControlNoWinControl ActiveX Implementation extern const GUID *rgBCFControlNoWinPropPages []; DEFINE_WINDOWLESSCONTROLOBJECT(BCFControlNoWin, &CLSID_BCFControlNoWin, "BCFControlNoWinCtl", CBCFControlNoWinControl::Create, 1, &IID_IBCFControlNoWin, "BCFControlNoWin.HLP",
&DIID_DBCFControlNoWinEvents, OLEMISC_IGNOREACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_RECOMPOSEONRESIZE | OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT | OLEMISC_ACTSLIKEBUTTON, POINTERINACTIVE_ACTIVATEONENTRY | POINTERINACTIVE_DEACTIVATEONLEAVE | POINTERINACTIVE_ACTIVATEONDRAG, TRUE, // control is opaque RESID_TOOLBOX_BITMAP2, "BCFControlNoWinWndClass", 1, rgBCFControlNoWinPropPages, 0, NULL);
Unclipped Device Context Unclipped device context is an MFC specific optimization. The flag results in only a single operation if (nFlags & clipPaintDC) dc.IntersectClipRect(rcClient); which can be found in the COleControl::OnPaint() function. The net result of this function call is to reduce the size of the area that will be drawn to.
Flicker-Free Activation Flicker-free activation is based on the IOleInPlaceSiteEx interface. The BaseCtl frame -work automatically attempts to find this interface, so it requires no implementation on the part of the developer. See the VC++ documentation for more information about the IOleInPlaceSiteEx interface.
Mouse Pointer Notifications When Inactive Only the windowless control BCFControlNoWin will take advantage of mouse pointer notifications
when inactive. To enable mouse pointer notifications, you must declare your control, as in Listing 11.51.
Listing 11.51 BCFCONTROLNOWIN.H--Mouse Notifications extern const GUID *rgBCFControlNoWinPropPages []; DEFINE_WINDOWLESSCONTROLOBJECT(BCFControlNoWin, &CLSID_BCFControlNoWin, "BCFControlNoWinCtl", CBCFControlNoWinControl::Create, 1, &IID_IBCFControlNoWin, "BCFControlNoWin.HLP", &DIID_DBCFControlNoWinEvents, OLEMISC_IGNOREACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_RECOMPOSEONRESIZE | OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT | OLEMISC_ACTSLIKEBUTTON, POINTERINACTIVE_ACTIVATEONENTRY | POINTERINACTIVE_DEACTIVATEONLEAVE | POINTERINACTIVE_ACTIVATEONDRAG, TRUE, // control is opaque RESID_TOOLBOX_BITMAP2, "BCFControlNoWinWndClass", 1, rgBCFControlNoWinPropPages, 0, NULL); The flags for a windowless control also differ slightly from its windowed counterpart. For your implementation, specifying OLEMISC_ACTIVATEWHENVISIBLE indicates that the control should become active as soon as it is visible. By specifying OLEMISC_IGNOREACTIVATEWHENVISIBLE, you instruct the control not to become active until some form of user action on the control takes place-that is, provided that the container supports the IPointerInactive interface. If the container does not provide the IPointerInactive interface, your control will be active the entire time it is visible.
Optimized Drawing Code Optimized drawing is handled much the same way it is in MFC and ATL. (Refer to the section on optimized drawing at the beginning of this chapter for more information.) A parameter of the OnDraw method indicates whether the control can draw using the optimized techniques first shown in the MFC implementation. In addition, the BaseCtl implementation allows for aspect or optimized drawing. Drawing with aspects is beyond the scope of this book. If you want to implement aspects, please see the OC 96 specification included in the ActiveX SDK.
Loads Properties Asynchronously To support asynchronous properties, a control must support the stock property ReadyState. The control is responsible for updating the property and notifying the container when it has changed. (See the section on asynchronous properties at the beginning of this chapter for more information.)
From Here... The BaseCtl framework provides a sound platform for control development. The lack of common functionality support that is equivalent to that of MFC is probably its biggest weakness. The amount of control and flexibility over your development is probably its greatest strength. It is interesting to note that the shortcomings of the BaseCtl framework are conversely proportional to the strengths of MFC, and vice versa. Creating a control using MFC and then porting it to the BaseCtl framework will give you a true appreciation and understanding of control and container development and architecture. It will also aid greatly in other areas of ActiveX/COM development. As far as ATL is concerned, BaseCtl is very similar in its style of implementation. The COM interfaces are the root of your control implementation, as it should be, and not a set of all-encompassing classes (as in MFC). A large number of parallels can be drawn between ATL and BaseCtl. For those developers who are really interested in how ActiveX and COM works in a control implementation, the best choice is to take a look at the BaseCtl. Probably the greatest limitation to using the BaseCtl is that it's considered to be an unsupported tool and is provided by its authors merely as a sample of how to do control development. You need to take this into consideration when deciding which method to use when developing your ActiveX components.
Chapter 12 Creating ActiveX COM Objects and Custom Interfaces Using MFC ●
Creating ActiveX COM Objects and Custom Interfaces Using MFC ❍ Anatomy of a COM Object ❍ Tools Needed for Building COM Objects ■ MIDL Compiler ■ GUIDGEN ■ RegEdit ■ Registration Server ■ Adding the Tools to the Visual C++ Development Environment ❍ Defining COM Interfaces Using IDL ■ Creating the IFISH Project ■ Creating the Interface Definition ■ Listing 12.1 IFISH.IDL--Interface Definition for IFish ■ Listing 12.2 IBASS.IDL--Interface Definition for IBass ■ Compiling the Interface Definition Files ■ Creating a Definition File ■ Listing 12.3 DLL LIBRARY--Definition File for IFISH.DLL ■ Adding the RPC Libraries to the Interface Project ■ Listing 12.4 RPCHELP.C--Compiler Pragmas Used for Referencing RPC Libraries ■ Registering the Interfaces ■ Listing 12.5 IFISH.REG--Contexts of IFISH.REG File Used to Register the Interfaces Supported by the IFISH.DLL ❍ Implementing the Interface ■ Using the Visual C++ AppWizard to Create the COM Object ■ Accessing In-Process COM Objects ■ Listing 12.6 BASS.DEF--BASS Definition File with the COM Support Functions Explicitly Exported ■ Listing 12.7 BASS.CPP--DLlGetClassObject Implementation Code Inserted by the MFC AppWizard ■ Listing 12.8 DLLCanUnloadNow--Implementation Code Inserted by the MFC AppWizard ■ Listing 12.9 DllRegisterServer--Implementation Code Inserted by the MFC AppWizard ■ Creating a Class that Implements COM Interfaces ■ Listing 12.10 COMMACROS.H--COM Macros Used for Accessing the IUnknown Implementation of CCmdTarget ■ Listing 12.11 BASSID.H--Header File bassid.h that Contains the Implementation of CLSID for the CBass Class ■ Listing 12.12 BASS.H--Header File for the CBass Class (bass.h) ■ Listing 12.13 BASS.CPP--Complete Implementation File for CBass object (Bass.cpp) ❍ Using the Interface ■ OLE Initialization and Shutdown Functions ■ Listing 12.14 COMTEST.CPP--Initialization and Removal of OLE Libraries within an MFC
❍
Application ■ COM Object Access Functions ■ Listing 12.15 Comtestview.cpp--Test Function Used for Accessing the IFish and IBass Interfaces From Here...
Creating ActiveX COM Objects and Custom Interfaces Using MFC ●
●
●
●
●
Supporting COM with MFC While MFC does not utilize COM directly, the MFC architects did provide support mechanisms that make adding COM functionality an easy task. Adding COM tools to the Visual C++ development environment The implementation of COM Objects requires new tools, such as the MIDL compiler, that are outside the scope of traditional application development. Creating a basic Component Object using MFC Examine the implementation and details of the two types of component objects, in-process (DLL) and outof-process (EXE). Defining a COM interface using the Interface Definition Language (IDL) The Interface Definition Language is used for defining your COM Object interface. Setting up and installing COM Objects Installing and using a COM Object is a straightforward process, but you need to be aware of a few trouble spots.
ActiveX is a strategic technology base for Internet programming and distributed computing. While ActiveX is the successor for OLE (Object Linking and Embedding), OLE still forms the foundation of ActiveX programming. The basis for ActiveX is to provide an object-oriented solution for solving problems encountered in developing operating systems and applications. ActiveX provides the specifications necessary to create component software that ultimately benefits the computing industry. At the core of ActiveX is an extremely powerful and extensible architecture called the Component Object Model (COM). COM provides a simple yet elegant solution for solving complex software problems such as accessing objects and services outside of application boundaries and version control. COM solves these problems through the use of binary components that are running in the system rather than by developing source code components within an application. If you are using the Visual C++ compiler from Microsoft, chances are very high that you are also using the Microsoft Foundation Classes (MFC) as the building blocks for your applications and components. MFC is a powerful set of base classes that provide a framework of abstractions into the Windows SDK for developing Win32 applications and components. Classes within the MFC framework are not directly derived from COM interfaces. However, the architects of MFC
have provided direct support for adding COM to any MFC-based component or application. The roots for supporting COM within MFC lie in wrappers called Interface maps. Interface maps are similar to message maps (which are used for distributing Windows messages to MFC classes) in both concept and execution.
Anatomy of a COM Object COM Objects give software developers the ability to leverage object-oriented software techniques for solving application and operating system development issues. The COM specification is not geared toward a specific language, although C++ is a natural choice when developing COM Objects. Four basic components compose a COM Object: ●
●
●
●
Classes--A class is a data structure with a set of interfaces used for accessing and manipulating the data structure. This definition is analogous to C++ class. The difference is that COM allows a class created in any language to be registered with the operating system and to be used in a language-independent manner. Objects--An object is an instance of a class created during program execution. In C++, an object is typically created via the new operator. When using COM, COM Objects are created through the function CoCreateInstance. Many instances of an object can be created. Interfaces--An interface is a group of functions (often called methods) that are part of a class. The interface functions are used to directly manipulate the data in a class. ActiveX is based on a set of COM interfaces. Of the set of ActiveX interfaces, the two that must be supported by COM Objects are IUnknown and IClassFactory. GUIDs--A GUID (Global Unique Identifier) is an 8-byte number that provides a unique identifier for each COM Object. GUIDs are generated by a tool called GUIDGEN. Each COM class must have two GUIDs, one for the Class ID and one for the Interface ID.
The class ID (CLSID) is an identifier for the COM class. This key is registered in the Windows Registry and contains a pointer (path) to where the DLL (Dynamic Link Library) or EXE containing the class can be located. The CLSID can be found in the Windows Registry under the path HKEY_CLASSES_ROOT\CLSID. The Interface ID (IID) is an identifier for the interface to the class. The IID is used by applications to query and invoke the methods into the class. The IID is also contained in the Windows Registry and can be found in the path HKEY_CLASSES_ROOT\Interface. Figure 12.1 illustrates the relationship among class, interfaces, and IID. FIG. 12.1 Relationship of COM Classes and Interfaces. What COM provides to software developers is an object-oriented solution for building and maintaining software solutions. Programmers using non-object-oriented languages such as Visual Basic can develop and use COM components to build software solutions. COM also provides a unique solution to the version control problems present in many of today's software solutions. Since COM Objects are binary components, developers do not have to worry about new or updated versions of a component being placed on a computer where their application is running. The reason for this is that COM deals with interfaces. If an interface is enhanced, new methods can be added to the interface, or additional interfaces can be obtained without breaking an existing application. COM's solution to version control provides a great method for upgrading applications while preserving legacy systems.
Tools Needed for Building COM Objects When creating your COM Objects, a few tools must be installed on your computer. Most of these tools are automatically installed as part of the Visual C++ development environment.
MIDL Compiler The Microsoft MIDL compiler is now a standard component of the Microsoft Visual C++ environment. The MIDL compiler compiles COM interface definitions into C code, which is then compiled into the project by the Visual C++ compiler. Figure 12.2 illustrates the purpose of the MIDL compiler. FIG. 12.2 Inputs and outputs of the MIDL compiler. The MIDL compiler also provides support for marshaling interfaces across process boundaries. Starting with Visual C++ 4.0, the MIDL compiler was shipped as a standard component of Visual C++. The MIDL compiler is also available with the Win32 SDK from Microsoft.
GUIDGEN GUIDGEN is a tool used to generate Global Unique Identifiers (GUID), which can be used for Interface IDs, Class IDs, or any other 128-bit UUID, such as an RPC interface. GUIDGEN is installed when the OLE development option is selected during the Visual C++ installation. When GUIDGEN is run, you must select the proper format for the UUID and then press the New GUID button to copy the UUID to the Windows Clipboard. After running the GUIDGEN application, the resulting GUID is pasted from the Clipboard into the code that needs a GUID.
NOTE: The tool GUIDGEN is also installed by default if the option Typical is selected during the Visual C++ 5.0 installation.
RegEdit RegEdit or the registration editor is a standard component of both the Windows 95 and Windows NT operating systems. The registration editor is used for browsing and altering operating system and application settings. The registration editor can also be used for installing and registering your COM Objects.
CAUTION: RegEdit is a powerful tool and must be used with extreme caution by experienced users. If used improperly, systems can be damaged, resulting in a loss of data or a malfunctioning computer.
In Windows 95, this program is called regedit.exe. In Windows NT, this program is called regedt32.exe.
Registration Server
The registration server is an application that can be used to register the settings of a COM Object in the Windows registry without the need to create a separate registration file. The application is called regsvr32.exe and is automatically installed if the OLE development option is selected during Visual C++ installation or if the ActiveX SDK is installed.
Adding the Tools to the Visual C++ Development Environment In order to maximize development productivity, the tools needed for COM programming should be integrated into the Visual C++ environment. Each of the tools needed can be added to the IDEs (Integrated Development Environment) Tools menu. The following sections illustrate how to incorporate the tools into the IDE. Adding the MIDL Compiler to the IDE Adding the MIDL compiler to the IDE allows for easy compilation of an IDL (Interface Definition Language) file. After adding this command, an IDL file can be compiled, and the MIDL compiler will generate a C source file with all appropriate parameter marshaling code. To add the MIDL compiler to the Visual C++ environment: 1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog. 2. In the Command edit box, type MIDL.EXE. 3. In the Menu contents edit box, type Compile &IDL File. 4. In the Arguments edit box, type /ms_ext /char unsigned /c_ext $FileName. 5. In the Initial directory edit box, type $FileDir. 6. Click the check box Use Output Window (the box should already be checked). 7. In the completed Customize dialog, click the Close button to add the entry to the Tools menu (see fig. 12.3). FIG. 12.3 Add your tools settings for the MIDL compiler in the Customize dialog. Adding GUIDGEN Adding GUIDGEN to the Visual C++ environment enables the generation of a UUID from a single menu command. As stated earlier, the generated UUID is placed in the Windows Clipboard and must be pasted into the project code. To add GUIDGEN to the Visual C++ development environment: 1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog. 2. In the Command edit box, type GUIDGEN.EXE. 3. In the Menu contents edit box, type &Generate New UUID. 4. Clear all text from the Arguments edit box. 5. Clear all text from the Initial Directory edit box. 6. Click the Close button to add the entry to the Tools menu. Adding the Registry Editor The Registry Editor serves two purposes: to add registration information to the
Windows registry and to browse the registry to view information. To add the Registry Editor to the Visual C++ environment, follow these steps: 1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog. 2. In the Command edit box, type REGEDIT.EXE if the development platform is Windows 95. Type REGEDT32.EXE if the development platform is Windows NT. 3. In the Menu contents edit box, type &Registry Editor. 4. Clear all text from the Arguments edit box. 5. In the Initial directory edit box, type $FileDir. 6. Click the Close button to add the entry to the Tools menu. Adding the Registration Server If the ActiveX Control option was selected during the Visual C++ installation, the registration server is already installed as the Register Control command from the Tools menu. If you have not installed the ActiveX Control option, installing it now adds the appropriate files and menu items to the Visual C++ development environment.
Defining COM Interfaces Using IDL A COM interface is a group of functions used to manipulate the data of the class that is implementing the interface. Interfaces only define the functions that belong to the group. An interface does not implement the function or contain data. The function implementation and data belong to the class that implements the interface. ActiveX is based entirely on a set of COM interfaces. These COM interfaces are a standard part of the operating system. In other words, Windows 95 and Windows NT contain all of the code that implement the ActiveX COM interfaces. When building new components based on COM, these components define custom interfaces. A custom interface is an interface that is not already supported by the operating system. A custom interface contains a set of functions that are specific to the new component being built. For example, a spell-checker component may contain a custom interface that contains a set of functions used by a program that uses the spell-checker component. Once an interface is defined, multiple components may be built that support and implement the interface. Going back to the spell-checker component, a defined spell-checker interface may be implemented by multiple companies. Having multiple companies provide a component with the same interface gives application developers the flexibility to have all of the components exist on a system, yet provide the user with the ability to load and use a specific company's spelling checker. When creating a custom interface, the interface definitions need to be shared among multiple applications, such as the server that implements the interface and the client that uses the interface. For this reason, it makes sense to define the interfaces in a project separate from the server or client projects. Multiple interfaces can be defined within a single project. In this chapter, we develop three projects to implement and use COM Objects. Table 12.1 shows the project names and the purpose of each project.
Table 12.1 Chapter 12 Project Descriptions Project Name
Purpose
IFISH (IFISH.DLL)
Implements a COM interface definition.
BASS (BASS.DLL)
Contains an MFC class that implements the COM interfaces in IFISH.
COMTEST (COMTEST.EXE)
Sample Test application that uses the BASS COM Object.
The project IFISH defines two COM interfaces, the IFish interface and the IBass interface. The IFish interface is a base class for all of the different species of fish. The IBass interface is an interface specific to a particular type of fish. Both of these interface definitions will be implemented within the IFISH project.
Creating the IFISH Project The IFISH project contains two COM interface definitions, IFish and IBass. The project IFISH is implemented as a DLL. The DLL does not contain any MFC code or written C\C++ code. The code contained within IFISH is produced by the MIDL compiler. The MIDL compiler takes the interface definition files (IDL) as input and produces C code for the interface as output. The C code that is produced is needed to implement parameter marshaling. Parameter marshaling is needed if the COM interface is implemented in an executable (EXE). The marshaling allows the parameters to be passed across process boundaries. Even if the COM interface implementation is in a DLL (in-process server), the MIDL compiler should still be used. There are no penalties for implementing parameter marshaling.
NOTE: The IFISH project is built as a DLL. This is not the DLL that is implementing the COM interface. IFISH contains only the interface definitions. Projects that contain the interface definitions should be implemented as DLLs.
Perform the following steps in order to create the IFISH project: 1. From within the Visual C++ development environment, select the command New from the File menu. 2. Select Projects tab from the New dialog. 3. Select Dynamic Link Library (see fig. 12.4). Enter the project name into the Project name edit box. The project is named IFISH. Select the OK button. FIG. 12.4 Select the project attributes for IFISH in the New dialog. 4. The project IFISH is now created.
Creating the Interface Definition When creating the interface definition, you must determine whether marshaling code is needed to provide support
for passing parameters between two processes. The safest method is to always assume that marshaling is needed. Providing marshaling support also allows the freedom to create either an in-process server (DLL) or an out-ofprocess server (EXE) to implement the interface. Use of the Microsoft RPC MIDL compiler provides parameter marshaling support. Parameter marshaling is automatically provided by defining the COM interface with the Interface Definition Language (IDL). Once the interface is defined using IDL, the RPC MIDL compiler automatically generates the code necessary for marshaling support. Two interface definition files are used in the IFISH project, IFISH.IDL and IBASS.IDL (see Listing 12.1).
Listing 12.1 IFISH.IDL--Interface Definition for IFish [ object, uuid(011BB310-5AB0-11d0-907E-00A0C91FDE82), pointer_default(unique) ] interface IFish : IUnknown { import "unknwn.idl"; HRESULT IsFreshwater([out] BOOL *pBool); HRESULT GetFishName([out, string, size_is(255)] char *p); } All interface definition files have the extension IDL. The first portion of the IDL file contains an object definition section. The most important part of this section is the UUID of the object. The UUID is a unique 128-bit number that is created through the tool GUIDGEN. The UUID in IFISH.IDL distinctly identifies the IFish COM interface definition. This number is used by applications that will use the IFish interface. A unique UUID number can be generated by performing the following steps: 1. From the Visual C++ development environment, select the command Generate New UUID, located under the Tools menu. (Note: This command was added earlier to the Tools menu, as shown in the section "Adding GUIDGEN"). The Create GUID dialog is displayed (see fig. 12.5). FIG. 12.5 Use the Create GUID dialog to generate unique identifiers for COM interfaces. 2. From the dialog, under GUID Format, select Registry Format [i.e., {XXXXXXX-XXXX...XXXX}]. 3. Select the Copy button. This option copies the new UUID into the Windows Clipboard. 4. Select Exit to close the GUIDGEN dialog. 5. In the interface definition file, paste the contents of the Clipboard (that is, the new UUID) into the UUID section of the IDL file. This is the unique identifier used for your interface. If another GUID is needed, you can press the New GUID button and then copy the second GUID from the Clipboard. Following the object definition section is the actual interface definition. As shown in Listing 12.1, the object definition resembles a C++ class definition. The keyword interface specifies the start of an interface definition. The
name of the interface and any inherited interfaces follows. In this case, the interface name is IFish, and this interface inherits the IUnknown interface. Since the IUnknown interface is a standard interface within the operating system, you don't need to redefine the functions within the interface. You only need to import the IUnknown interface definition. You do this through the statement import "unknwn.idl";. The functions implemented by the interface need to be added in order to complete the interface definition. When using IDL, all portions of a function must be defined: the return value and all parameters, including direction (in, out, or both) and size of the parameters. Specifying all portions of a function allows the MIDL compiler to generate the correct marshaling code for the interface.
NOTE: All return values must be of type HRESULT, which is standard OLE return value. If the return value is not an HRESULT, the MIDL compiler will not provide marshaling information to marshal across process boundaries. The return value HRESULT is needed for network support. In case a network error occurs, a valid error code can be returned without having to generate an exception.
The interface for IBASS is shown in Listing 12.2. Note that IBass is an aggregate interface, not an inherited interface. Aggregate interfaces will be explained in the section "Implementing the Interface."
Listing 12.2 IBASS.IDL--Interface Definition for IBass [ object, uuid(F60D7C40-5B4E-11d0-ABE6-D07900C10000), pointer_default(unique) ] interface IBass : IUnknown { import "unknwn.idl"; HRESULT GetLocation([out, string, size_is(255)] char *p); HRESULT SetLocation([in, string] char *p); HRESULT EatsOtherFish([out] BOOL *pBool); }
Compiling the Interface Definition Files After the respective IDL files have been created, they must be compiled in order for the interface code to be generated. Since the MIDL compiler was added to the project in the section "Adding the MIDL Compiler to the IDE," this task is an easy one. To compile the IDL files, perform the following steps: 1. Load the file IFish.IDL into the Visual C++ environment. 2. Select the command Compile IDL from the Tools menu in the Visual C++ development environment. 3. Load the file IBass.IDL into the Visual C++ environment.
4. Select the command Compile IDL from the Tools menu in the Visual C++ development environment. 5. The MIDL compiler has now compiled the interface definition files and generated the source code for supporting these interfaces. You may be surprised to see the code that is generated by the MIDL compiler from the simple interface definitions IFish and IBass. Unlike a C++ compiler, the output from MIDL is not binary code. Instead, MIDL generates C code, which is then compiled by the C compiler as part of the interface project. When the file IFISH.IDL was compiled, the files shown in Table 12.2 were generated. Table 12.2 Results Produced by Compiling IFISH.IDL File
Purpose
IFISH.H
Support header file for the IFish interface.
IFISH_I.C
Interface definition file that is added to both the server and client projects.
IFISH_P.C
Proxy code that implements the marshaling code for the interface.
DLLDATA.C Reference file used for loading the correct interface from the DLL. Shared by all IDL files compiled within this project.
The files that were created by the MIDL compiler must now be added to the IFISH project.
NOTE: When the IFISH project was created, no source files were included in the project. The entire project consisted of only a MAK file. Since this is an interface-only DLL, the entire contents of the project will consist of the files created via the MIDL compiler.
The following files must be added to the IFISH makefile in order for the IFish and IBass interface definitions to be accessible and used in COM Object implementations. ifish.h ibass_i.c ifish_i.c ibass_p.c ifish_p.c Dlldata.c ibass.h Rpchelp.c You can add these files to the project by performing the following steps: 1. From the Visual C++ development environment, select the Files command from the Add to Project menu item, which can be accessed from the File menu. The Insert Files into Project dialog is displayed (see fig. 12.6). FIG. 12.6 The Insert Files into Project dialog is used for adding MIDL files into a project. 2. Select the files shown in the preceding file list, and click OK. The files are now added to the interface project make file.
Creating a Definition File One of the tedious tasks that, unfortunately, is not performed by either the Application Wizard or the MIDL compiler is the creation of a library definition file (DEF). This library definition file, a standard part of DLLs, defines which functions are exported or made accessible by the DLL. The filename is IFISH.DEF.
NOTE: Since a standard Win32 DLL was created, there were no functions to be exported because there were no source files when the file was created. This is not the case when an MFC DLL is created. In that case, MFC source code is produced, and a DEF file for the project is also created with default functions exported through the DEF file.
The contents of the IFISH.DEF file were created manually and can be viewed in Listing 12.3.
Listing 12.3 DLL LIBRARY--Definition File for IFISH.DLL LIBRARY IFISH DESCRIPTION `IFISH Interface Marshaling' EXPORTS DllGetClassObject DllCanUnloadNow The DLL entry points must be defined because of the parameter-marshaling code generated by the MIDL compiler. The MIDL compiler generates code that uses the IMarshall interface. The IMarshall interface requires the DLL entry point's DllGetClassObject and DllCanUnloadNow. The IMarshall interface is a COM interface that implements parameter marshaling for all COM Objects. These two entry points are explained in greater detail in the section "Accessing In-Process COM Objects."
Adding the RPC Libraries to the Interface Project Parameter marshaling is implemented through RPC (Remote Procedure Calls) libraries. When creating interface libraries that use RPC for parameter marshaling, you must link a number of RPC libraries into the interface project. You can select from two methods for linking the RPC libraries into the interface project: ● ●
Add the library names to the list of input libraries used when linking the project. Create a file with compiler pragmas that reference the libraries.
Four RPC libraries must be included in the interface project, rpcndr.lib, rpcdce4.lib, rpcns4.lib, and rpcrt4.lib. Creating a file with compiler pragmas is much easier than trying to remember these library filenames and including them for each project that defines a COM interface. In the IFISH project is a file called RPCHELP.C (see Listing 12.4). This file contains the necessary compiler pragmas for RPC support.
Listing 12.4 RPCHELP.C--Compiler Pragmas Used for Referencing RPC Libraries
#pragma comment(lib, "rpcndr.lib") #pragma comment(lib, "rpcns4.lib") #pragma comment(lib, "rpcrt4.lib") The file RPCHELP.C must be added to the IFISH project in order for the project to link properly. The file can be added to the project by performing the following steps: 1. From the Visual C++ development environment, select the Files command from the Add to Project menu item, which can be accessed from the File menu. 2. Select the files shown in the file list provided earlier in this chapter, and click OK. The files are now added to the interface project make file. The interface definitions for IFISH are now complete, and the project is ready to be built. Building the project generates a DLL called IFISH.DLL.
Registering the Interfaces Only one task remains before the IFish and IBass interfaces can be used. The interfaces must be registered in the Windows registry. The Windows registry is the holding ground for all class and interface IDs. For the IFISH project, a registration file named IFISH.REG must be manually created. The contents of IFISH.REG are shown in code Listing 12.5.
Listing 12.5 IFISH.REG--Contexts of IFISH.REG File Used to Register the Interfaces Supported by the IFISH.DLL HKEY_CLASSES_ROOT\Interface\{011BB310-5AB0-11d0-907E-00A0C91FDE82} HKEY_CLASSES_ROOT\Interface\{011BB310-5AB0-11d0-907E-00A0C91FDE82} \ProxyStubClsid32 HKEY_CLASSES_ROOT\CLSID\{011BB310-5AB0-11d0-907E-00A0C91FDE82} = IFish_PSFactory HKEY_CLASSES_ROOT\CLSID\{011BB310-5AB0-11d0-907E-00A0C91FDE82}\InprocServer32 = d:\dev\ifish\debug\ifish.dll HKEY_CLASSES_ROOT\Interface\{F60D7C40-5B4E-11d0-ABE6-D07900C10000} HKEY_CLASSES_ROOT\Interface\{F60D7C40-5B4E-11d0-ABE6-D07900C10000} \ProxyStubClsid32 HKEY_CLASSES_ROOT\CLSID\{F60D7C40-5B4E-11d0-ABE6-D07900C10000} = IBass_PSFactory HKEY_CLASSES_ROOT\CLSID\{F60D7C40-5B4E-11d0-ABE6-D07900C10000}\InprocServer32 = d:\dev\ifish\debug\ifish.dll To add the interface keys to the Windows registry, do the following: 1. Select the command Registry Editor from the Tools menu of the Visual C++ development environment. 2. Select the command Import Registry File from the Registry Editor File menu. 3. Select the file IFISH.REG, and then select OK.
Implementing the Interface Now that the IFish interface definitions are defined, the object that implements the interfaces must be created. Remember that the IFISH interface DLL contains only the interface definitions and RPC proxy code for parameter marshaling. There is no code for implementing the interface within IFISH.DLL. The COM Object developed in this section in the project BASS.DLL will contain an MFC class called CBass that will implement both the IFish and IBass interfaces. COM Objects can be implemented as either a DLL or an EXE. COM Objects that reside within a DLL are called inprocess servers. COM Objects that reside in an EXE are called out-of-process servers. The application that will use the COM Objects does not see a difference between the two types of servers. However, internally, there are a few differences between in-process servers and out-of-process servers. ●
●
An in-process server typically provides better load time performance. COM Object load time is faster with an in-process server due to the COM Object being contained in a DLL. DLLs typically load faster than executables. COM Objects in an in-process server also execute faster because parameter marshaling does not need to occur when passing information from the calling application to the COM Object.
CAUTION: If DCOM is in your development plans, you have one detriment to in-process servers: DCOM or the Distributed Component Object Model allows components to reside on other computers in your network environment. This feature allows the components to utilize the processing power of other workstations. If you are planning for DCOM, you must implement your COM Objects as out-ofprocess servers because DLLs cannot be used across machine boundaries.
Using the Visual C++ AppWizard to Create the COM Object Nothing special is needed to create a basic application for containing your COM Objects. The application is where the COM interface definitions are implemented. With this in mind, you can create a basic COM Object application. In this section, you will create a COM Object using an MFC DLL (in-process server) as the containing application. During creation of the application, the differences between creating an in-process and out-of-process server will be pointed out.
NOTE: The differences between an in-process and out-of-process server are trivial at this point because the interface DLL already contains the proxy code used for parameter marshaling. The determination to have an in-process server versus an out-of-process server depends on the use and needs of the COM Object.
To create the basic application, perform the following steps: 1. In the Visual C++ development environment, select the New command from the File menu.
2. From the Projects tab on the New dialog, select MFC AppWizard (dll). (See fig. 12.7.) Enter the project name into the Project name edit box. This project is called BASS. Select the OK button. FIG. 12.7 Name the COM Object in the New Project Workspace dialog. NOTE: When creating an out-of-process server, select the MFC AppWizard (exe) item in the New Project tab. 3. From the MFC AppWizard dialog, select Regular DLL with MFC statically linked or select Regular DLL using shared MFC DLL (see fig. 12.8), depending on your needs. Also select the Automation option in the MFC AppWizard dialog. The Automation option will cause the AppWizard to insert start-up and exit code used for both OLE Automation Servers and COM Objects. Select the Finish button when completed. The New Project Information dialog will confirm your choices (see fig. 12.9). FIG. 12.8 Choose your project build options. FIG. 12.9 Recap the project selections in the New Project Information dialog.
TIP: Your COM Objects will be easier to distribute if you use the static-linked version of MFC. If your COM Objects are part of a project that contains other MFC components and applications, you will get better performance by using the DLL version of MFC.
At this point, you have a basic shell application that can be used for your COM Objects.
Accessing In-Process COM Objects In-process servers contain two functions that serve as entry points for clients accessing COM Objects. These functions are DllGetClassObject and DllCanUnloadNow. These functions are not needed for EXE or outof-process servers. In order for the COM support functions to be accessed, the functions must be exported from the DLL. An exported function can be called from any Windows application. The COM support functions are defined in MFC but are implemented in the server DLL. These support functions are also exported through the definition file (.DEF) of the DLL that uses the functions--in this case, BASS DLL. The AppWizard has already created a DEF file entitled BASS.DEF (see Listing 12.6).
Listing 12.6 BASS.DEF--BASS Definition File with the COM Support Functions Explicitly Exported ; BASS.def : Declares the module parameters for the DLL. LIBRARY "BASS" DESCRIPTION `FISH Windows Dynamic Link Library' EXPORTS
; Explicit exports can go here DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE DllRegisterServer PRIVATE
NOTE: The BASS definition file and the three support functions needed for accessing COM Objects were automatically inserted by the MFC Application Wizard as a result of selecting the OLE Automation option for the project. Automation and COM Objects are accessed in in-process servers through the same support functions. If OLE Automation was not selected, these functions will have to be implemented manually.
Each of the COM support functions are explained in the following three sections. HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, LPVOID *ppv) When a user requests a given COM Object, the Component Object Library looks into the Windows registry for the InProcServer of the given CLSID. The DLL that implements the COM Object is then loaded into memory, and the function DllGetClassObject is called. The CLSID of the COM Object implementing the interface and IID of the interface the user is requesting are passed into the function. The DLL containing the COM Object then creates the appropriate class factory for the CLSID and returns the corresponding interface pointer for the IID. Since a CLSID is passed into DllGetClassObject, a DLL can contain many different COM Objects. The interface pointer is returned to the caller through the parameter ppv. The MFC AppWizard inserts all of the code needed for accessing COM Objects in an MFC Application DLL. The code for DllGetClassObjects is shown in Listing 12.7.
Listing 12.7 BASS.CPP--DLlGetClassObject Implementation Code Inserted by the MFC AppWizard STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllGetClassObject(rclsid, riid, ppv); } STDAPI DllCanUnloadNow(void) Since the client application using the COM Object does not link directly with the server creating the COM Object, an unload mechanism must be in place so that unneeded COM Objects are removed from memory. With in-process servers, this mechanism is through the function DllCanUnloadNow. The Component Object Library periodically asks each COM Object server if it can be unloaded from memory. If the server returns S_OK, the server DLL is removed from memory. A COM Object server returns S_OK when clients have finished using the COM Objects. The function DllCanUnloadNow is shown in Listing 12.8. An EXE server does not need to implement the function DllCanUnloadNow because an EXE can keep track of how many users it has. When the usage count reaches 0, the EXE can unload itself from memory.
Listing 12.8 DLLCanUnloadNow--Implementation Code Inserted by the MFC AppWizard STDAPI DllCanUnloadNow(void)
{ AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllCanUnloadNow(); } BOOL DllRegisterServer(void) The function DllRegisterServer is a useful method for having the COM Object server correctly register with the Windows registry. When this function is called, the COM Object server updates the Windows registry with the settings necessary for client applications to use the server. The ActiveX development kit has a tool called regsvr32.exe. This program can be run with two parameters. The first parameter is the DLL to register. The second parameter is TRUE (for registering a server) or FALSE for unregistering a server. To register the BASS COM server, run regsvr32 with the following parameters: regsvr32 BASS.DLL TRUE
Listing 12.9 DllRegisterServer--Implementation Code Inserted by the MFC AppWizard // by exporting DllRegisterServer, you can use regsvr.exe STDAPI DllRegisterServer(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); COleObjectFactory::UpdateRegistryAll(); return S_OK; }
Creating a Class that Implements COM Interfaces Now that the application that will contain the COM Class has been created, the COM Class needs to be added to the project. The Visual C++ ClassWizard will be used to implement this class. The class name being created is CBass. To create the CBass class, perform the following steps: 1. Select the ClassWizard command from the View menu of the Visual C++ development environment. The ClassWizard dialog will appear as shown in Figure 12.10. 2. Select the Add Class button, and then select the new option. The New Class dialog appears (see fig. 12.11). FIG. 12.11 Set the class options with the New Class dialog. 1. Enter the class name CBass in the Name edit box. 2. Select the base class CCmdTarget from the Base class drop-down list. 3. Select the Create button. The Class CBass is now part of the BASS project. The class CCmdTarget is chosen as the base class because MFC provides a standard implementation of the IUnknown interface in this class. As you have seen, the IUnknown interface provides the three basic methods that must be supported by all COM Objects.
Deriving from the CCmdTarget base class also allows the object to be created through the MFC class COleObjectFactory. The class COleObjectFactory is called through the DLL entry point, DllGetClassObject. If the COM class was not derived from a class with CCmdTarget as its base, special object creation code must be written in the function DllGetClassObject to create an instance of the object.
NOTE: Even though CCmdTarget was selected as the base class for the CBass class, any of the other classes derived from CCmdTarget can be used. If a class not derived from CCmdTarget is used, you must manually provide support for the IUnknown interface.
Figure 12.12 illustrates the CBass class and the interfaces that will be encapsulated within this class. FIG. 12.12 Class hierarchy and supported interfaces of the CBass class. Supporting the IUnknown Interface While the MFC class CCmdTarget provides built-in support for the IUnknown interface, the COM class derived from CCmdTarget must still provide methods that enable the MFC Interface maps to call these routines. Since IUnknown is a standard COM interface, the functions that must be called from the COM class have been implemented through a series of support macros. The advantage of the set of macros is that they can be used for the IUnknown implementation of any MFC derived class. The file commacros.h implements a set of macros that are used for calling the IUnknown implementation within the CCmdTarget class. Listing 12.10 shows the COM support macros for MFC.
NOTE: The file commacros.h is not a part of MFC and must be added to the project manually.
Listing 12.10 COMMACROS.H--COM Macros Used for Accessing the IUnknown Implementation of CCmdTarget #ifndef _COMMACROS_H #define _COMMACROS_H #ifndef IMPLEMENT_IUNKNOWN #define IMPLEMENT_IUNKNOWN_ADDREF(ObjectClass, InterfaceClass)\ STDMETHODIMP_(ULONG)ObjectClass::X##InterfaceClass::AddRef(void)\ { \ METHOD_PROLOGUE(ObjectClass, InterfaceClass); \ return pThis->ExternalAddRef(); \ } #define IMPLEMENT_IUNKNOWN_RELEASE(ObjectClass, InterfaceClass)\ STDMETHODIMP_(ULONG)ObjectClass::X##InterfaceClass::Release(void)\ { \ METHOD_PROLOGUE(ObjectClass, InterfaceClass); \ return pThis->ExternalRelease(); \ } #define IMPLEMENT_IUNKNOWN_QUERYINTERFACE(ObjectClass, InterfaceClass)\ STDMETHODIMP ObjectClass::X##InterfaceClass::QueryInterface(REFIID riid, LPVOID *pVoid)\
{ \ METHOD_PROLOGUE(ObjectClass, InterfaceClass); \ return (HRESULT)pThis->ExternalQueryInterface(&riid ,ppVoid); \ } #define IMPLEMENT_IUNKNOWN(ObjectClass, InterfaceClass)\ IMPLEMENT_IUNKNOWN_ADDREF(ObjectClass, InterfaceClass)\ IMPLEMENT_IUNKNOWN_RELEASE(ObjectClass, InterfaceClass)\ IMPLEMENT_IUNKNOWN_QUERYINTERFACE(ObjectClass, InterfaceClass) #endif #endif Adding a Class ID Now that the class that will support the COM interfaces is defined, a unique class ID (CLSID) must be created for the class. The CLSID allows the operating system to distinguish which application to load when a client program wants to invoke an instance of the object. To create a unique CLSID, the program GUIDGEN must once again be run.
NOTE: To create a unique ID using GUIDGEN, refer to the steps outlined in the section " Creating the Interface Definition."
After the CLSID is created, it must be placed in a header file that acts as a define for the class implementation. For the CBass object, the file bassid.h is created. The CLSID is then pasted into the file and added to the macro DEFINE_GUID (see Listing 12.11).
Listing 12.11 BASSID.H--Header File bassid.h that Contains the Implementation of CLSID for the CBass Class #ifndef _CLSID_Bass #define _CLSID_Bass //{AFA853E0-5B50-11d0-ABE6-D07900C10000} DEFINE_GUID(CLSID_Bass,0xAFA853E0,0x5B50,0x11d0, 0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00); #endif The macro DEFINE_GUID assigns the name CLSID_Bass to the class ID that was created via GUIDGEN. This macro is placed in a header file that is used by all clients that need to invoke an instance of CLSID_Bass. This file is not used by the server that implements the COM Object. Using Interface Maps to Support COM Interfaces MFC supports COM interfaces through a technique known as interface maps. This technique is similar to the message maps used to route Windows messages to message handlers (functions) within the target class. Interface maps are basically a set of macros that provide support for the COM interfaces embedded within the MFC-derived class. Interface maps determine which interface methods will be handled by the particular MFC derived class. Only methods placed in the interface maps are supported by the COM Object. Only interface functions declared within the interface map are supported by the COM Object.
NOTE: Even though the IUnknown interface functions are not included within the interface map for the class, they must be supported by the class. All derived methods of the interface must be supported, although not explicitly defined.
Adding Interface maps to a class is an easy procedure. To add interface maps to a class, perform the following steps: 1. Add the header files that define the interfaces to your class. In the BASS project, includes for the interface files Ifish.h and Ibass.h were added to the header file bass.h. The interface files ifish.h and ibass.h were generated by the MIDL compiler when the IFISH.DLL project was built. These files define the respective interface classes. 2. Add the macro DECLARE_OLECREATE() to the class. This macro adds public data members to the class including COleObjectFactory, which is the primary interface needed for creating an object of the specified class. 3. The COM specification dictates that interfaces simply define the interface and that they do not implement the methods for the interface or contain data members. This concept means that the class implementing the interface must contain the methods for the interface and contain any data variables needed to keep track of information about the interface. For the class CBass, Table 12.3 illustrates the data members needed for the interface implementation and the interface method that retrieves or sets the data member. 4. Now that the data members are added to the class, some MFC macros must be added to the class in order for MFC to support the interfaces. The first macro is DECLARE_INTERFACE_MAP, which is added to a protected section of the class. This define is analogous to the DECLARE_MESSAGE_MAP macro that is needed for support of Windows message maps. DECLARE_INTERFACE_MAP is a macro that adds member variables needed for the support of COM interfaces. 5. For each of the supported primary interfaces, there must be an interface map. An interface map is added to a class through the macros BEGIN_INTERFACE_PART (Class Name, Interface Name) and END_INTERFACE_PART (Class Name). Between these sections are the methods that implement the interface in the class. Again, the IUnknown interface methods do not need to be added.
NOTE: Upon inspection of the macro BEGIN_INTERFACE_PART, you can see that the methods for the IUnknown interface are automatically added to the class. This eliminates the need to manually add them to the class.
Table 12.3 Data Members Needed in CBass Class to Implement Supported Interfaces Data Member
Interface Method
BOOL m_bFreshwater
IFish::IsFreshwater
CString m_zFishName
IFish::GetFishName
CString m_zLocation
IBass::GetLocation IBass::SetLocation
BOOL m_bEatsOtherFish
IBass::EatsOtherFish
The header file for the CBass class (BASS.H) has been modified to include all of the changes listed in Table 12.3. The resultant file is shown in Listing 12.12.
Listing 12.12 BASS.H--Header File for the CBass Class (bass.h)
#ifndef __AFXWIN_H__ #error include `stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols #include "..\ifish\ifish.h" #include "..\ifish\ibass.h" //////////////////////////////////////////////////// /////////////////////////// CBass command target class CBass : public CCmdTarget { DECLARE_DYNCREATE(CBass) CBass(); // protected constructor used by dynamic creation // Attributes public: CString m_zFishName; CString m_zLocation; BOOL m_bEatsOtherFish; BOOL m_bFreshwater; // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CBass) //}}AFX_VIRTUAL // Implementation protected: virtual ~CBass(); // Generated message map functions //{{AFX_MSG(CBass) // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG DECLARE_MESSAGE_MAP() DECLARE_OLECREATE(CBass) DECLARE_INTERFACE_MAP() BEGIN_INTERFACE_PART(Fish, IFish) STDMETHOD(GetFishName)(char *pStr); STDMETHOD(IsFreshWater)(BOOL *pBool); END_INTERFACE_PART(Fish) BEGIN_INTERFACE_PART(Bass, IBass) STDMETHOD(GetLocation)(char *pStr); STDMETHOD(SetLocation)(char *pStr); STDMETHOD(EatsOtherFish)(BOOL *pBool); END_INTERFACE_PART(Bass) }; Implementing MFC Interface Maps In the previous section, interface maps were added to the class header file. The actual interface maps must now be implemented in order for the interfaces to be supported. These changes are made to the source implementation file for the class. For the CBass class, this file is bass.cpp. 1. Add the COM support macros to the implementation file. Add the line #include "commacros.h" to your class implementation file. Commacros.h is the common include file that implements the IUnknown interface for your MFC class.
2. Add the macro IMPLEMENT_OLECREATE() to the source file as shown here. This macro links the MFC class, in this case CBass, with the friendly or callable name for the class (Bass). The CLSID for this class is also passed into the macro. The IMPLEMENT_OLECREATE is needed for implementation of the COleClassFactory object that instantiates instances of the CBass object. //{AFA853E0-5B50-11d0-ABE6-D07900C10000} IMPLEMENT_OLECREATE(CBass, "Bass", 0xAFA853E0,0x5B50,0x11d0,0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00) 3. Implement the interface map for the class. This process is very similar to implementing the message map. You implement an interface map by using three macros: BEGIN_INTERFACE_MAP, INTERFACE_PART, and END_INTERFACE_MAP.
The macro BEGIN_INTERFACE_MAP takes two parameters: the runtime class (Cbass) and the derived class (CCmdTarget). This macro provides a series of methods used for retrieving entries into the interface map structure. The macro INTERFACE_PART is needed for each COM interface that is to be supported by the class. This macro links the runtime class with the UUID for the interface and the friendly interface name. The macro END_INTERFACE_MAP ends the interface map implementation. BEGIN_INTERFACE_MAP(CBass, CCmdTarget) INTERFACE_PART(CBass, IID_IFish, Fish) INTERFACE_PART(CBass, IID_IBass, Bass) END_INTERFACE_MAP(); 4. Now you implement the interfaces within the class. This step is where the methods for the interface are implemented. Adding methods to a class is a straightforward process, with one exception. The method definition is as follows: runtime_class::interface_class::method. Listing 12.13 illustrates the implementation file, bass.cpp. The macro METHOD_PROLOGUE is used to establish a local variable named pThis, which is a pointer to the interface function table. The arguments to the METHOD_PROLOGUE are the runtime class and the interface name. This macro must precede every interface implementation. 5. So far you have not implemented the IUnknown interfaces for the class. To implement the IUnknown interfaces, use the macro that was added in the commacros.h file. In bass.cpp, the following lines are added to implement the IUnknown interface for each interface. // implement the Iunknown interface IMPLEMENT_IUNKNOWN(CBass, Fish) IMPLEMENT_IUNKNOWN(CBass, Bass)
Listing 12.13 BASS.CPP--Complete Implementation File for CBass object (Bass.cpp) ///////////////////////////////////////////////////////////////////////////// // CBass IMPLEMENT_DYNCREATE(CBass, CCmdTarget) CBass::CBass()
{ m_zFishName = "Large Mouth Bass"; m_zLocation = "Under Lily Pads"; m_bEatsOtherFish = TRUE; m_bFreshwater = TRUE; } CBass::~CBass() { } BEGIN_MESSAGE_MAP(CBass, CCmdTarget) //{{AFX_MSG_MAP(CBass) // NOTE - the ClassWizard will add and remove mapping macros here. //}}AFX_MSG_MAP END_MESSAGE_MAP() //{AFA853E0-5B50-11d0-ABE6-D07900C10000} IMPLEMENT_OLECREATE(CBass, "Bass", 0xAFA853E0,0x5B50,0x11d0,0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00) BEGIN_INTERFACE_MAP(CBass, CCmdTarget) INTERFACE_PART(CBass, IID_IFish, Fish) INTERFACE_PART(CBass, IID_IBass, Bass) END_INTERFACE_MAP(); ///////////////////////////////////////////////////////////////////////////// // CBass message handlers // CBass:Fish implementation of IFish // implement the Iunknown interface IMPLEMENT_IUNKNOWN(CBass, Fish) STDMETHODIMP CBass::XFish::GetFishName( char *pStr) { METHOD_PROLOGUE(CBass, Fish); TRACE("CBass::XFish::GetFishName\n"); if (pStr) strcpy((char *)pStr, (LPCTSTR)pThis->m_zFishName); return (HRESULT)NOERROR; } STDMETHODIMP CBass::XFish::IsFreshwater( BOOL *pBool ) { METHOD_PROLOGUE(CBass, Fish); TRACE("CBass::XFish::IsFreswWater\n"); if (pBool) { *pBool = pThis->m_bFreshwater; return S_OK; } return (HRESULT)NOERROR; } // CBass:Fish implementation of IFish // implement the Iunknown interface IMPLEMENT_IUNKNOWN(CBass, Bass) STDMETHODIMP CBass::XBass::GetLocation( char *pStr) { METHOD_PROLOGUE(CBass, Bass); TRACE("CBass::XBass::GetLocation\n"); if (pStr)
strcpy((char *)pStr, (LPCTSTR)pThis->m_zLocation); return (HRESULT)NOERROR; } STDMETHODIMP CBass::XBass::SetLocation( char *pStr) { METHOD_PROLOGUE(CBass, Bass); TRACE("CBass::XBass::SetLocation\n"); if (pStr) pThis->m_zLocation = pStr; return (HRESULT)NOERROR; } STDMETHODIMP CBass::XBass::EatsOtherFish( BOOL *pBool ) { METHOD_PROLOGUE(CBass, Bass); TRACE("CBass::XBass::EatsOtherFish\n"); if (pBool) { *pBool = pThis->m_bEatsOtherFish; return S_OK; } // return E_BADPOINTER; return (HRESULT)NOERROR; } Building the COM Object The good news is that all of the code has been written for the COM Object. However, here are a couple of items that still need to be done before the COM Object can be built: ●
●
First you need to add the two MIDL-generated "C" files to the project. When the MIDL compiler generates the IID for the interface, only an external reference to the IID structure is created. In order to successfully build the project using the interfaces, the actual definition of the IID structure must be included. These definitions can be found in the files iXXX_i.c files. For the Bass project, these files are ifish_i.c and ibass_i.c. These MIDL-generated files must be added to the list of files compiled and built when creating the project. The Visual C++ project default option of precompiling headers using stdafx.h must be changed. The correct option for compiling projects that use MIDL-generated files is to automatically use precompiled headers. Automatic use of precompiled headers can be enabled by the following steps: 1. Select the Settings command from the Project menu. 2. Select the C/C++ tab in the Project Settings dialog. 3. Select precompiled headers from the Category_ drop-down list (see fig. 12.13). FIG. 12.13 Enabling automatic use of precompiled headers for building a COM Object. 4. Select Automatic use of precompiled headers. 5. Click the OK button.
Using the Interface Use of COM Objects within an application is an easy task. A test application called ComTest has been developed to aid in the testing and use of the IFish and IBass interfaces. Only a handful of functions are necessary to access and utilize COM interfaces within an application. These functions can be broken into two categories: ● ●
OLE Initialization and Shutdown functions COM Object access functions
OLE Initialization and Shutdown Functions When building MFC applications that will utilize COM interfaces, two functions must be called to properly initialize the MFC framework. These functions are AfxOleInit() and CoFreeUnusedLibraries(). These functions must be called during the application initialization and removal. Listing 12.14 shows how the OLE libraries are initialized for use during application creation and removed during exit.
NOTE: If the COMTEST app was created with OLE support, the AppWizard would have automatically inserted the function AfxOleInit() within the InitInstance() method of CComTestApp. Likewise, OLE termination code would be automatically called on the program's exit.
Listing 12.14 COMTEST.CPP--Initialization and Removal of OLE Libraries within an MFC Application CComTestApp::CComTestApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance AfxOleInit(); } int CComTestApp::ExitInstance() { ::CoFreeUnusedLibraries(); return CWinApp::ExitInstance(); }
COM Object Access Functions Using COM interfaces within an application is similar to using any ordinary C++ class, the exception being that instances of a class are created through the function CoCreateInstance() rather than the new operator. After an interface pointer is returned, it can be used as though it is a C++ class, to call any of the functions in the interface. Listing 12.15 provides an example of how to access COM interfaces and call their functions.
Listing 12.15 Comtestview.cpp--Test Function Used for Accessing the IFish and IBass Interfaces void CComTestView::OnEditCreatebassinterfaces() { char lo_Location[255]; char lo_FishName[255]; IFish *pIfish; IBass *pIBass; ::CoCreateInstance( CLSID_Bass, NULL, CLSCTX_INPROC_SERVER, IID_IFish, (LPVOID *)&pIfish); if ( pIfish ) { TRACE0("Success ... Got an interface to Ifish\n"); pIfish->GetFishName(lo_FishName); TRACE1("The Fish Name is %s\n", lo_FishName); if (pIfish->QueryInterface(IID_IBass, (LPVOID *)&pIBass)== S_OK) { pIBass->GetLocation(lo_Location); TRACE1(" The Fish is a bass and it is located %s\n", lo_Location); pIBass->Release(); } pIfish->Release(); } }
From Here... This chapter discussed creating COM Objects based on the MFC application framework. While MFC does not directly utilize COM, provisions have been made so that MFC supports COM and reduces the amount of work necessary in creating COM Objects. Other techniques can be used for creating COM Objects. Chapter 13 examines the creation of COM Objects through the ActiveX Template Library (ATL). ATL provides a lightweight COM framework for building COM Objects.
Chapter 13 Creating ActiveX COM Objects and Custom Interfaces Using ATL ●
Creating ActiveX COM Objects and Custom Interfaces Using ATL ❍ Reaping Benefits of the ActiveX Template Library ■ Support for Multiple Server Types ■ Threading Models Supported by ATL ■ Tear-Off Interfaces ■ Implementing Interface Aggregation ■ Built-In Support for Error Handling ❍ Creating a COM Server Using ATL ■ Using the ATL COM Wizard to Create a COM Server ■ Examining the Results of the ATL COM Wizard ■ Implementing the COM Server Access Functions ■ Listing 13.1 AtlCustomBass.def--Library Definition File for CAtlCustomBass ■ Listing 13.2 AtlCustomBass.cpp--ATL Implementation of DLL Access Functions for COM Server ■ Using IDL to Create Object Definitions ■ Listing 13.3 AtlCustomBass.idl--Adding Custom Interface Methods to the Project Interface Definition File ■ Implementing the COM Interface ■ Listing 13.4 AtlCustomBass.h--C++ Class Definition for the AtlCustomBass COM Server ■ Listing 13.5 CAltCustomBass1.cpp--Listing of CAltCustomBass1.cpp, Which Implements the COM Interfaces for IFish and IBass ■ Using Object Maps to Specify COM Objects ❍ When to Use the ActiveX Template Library ❍ From Here...
Creating ActiveX COM Objects and Custom Interfaces Using ATL ●
●
●
Benefits of using ATL ATL allows for the creation of many types of COM servers and COM models. Easy creation of COM interfaces and classes The ATL project wizard produces template code for the creation of COM classes and interfaces. Implementing tear-off interfaces A tear-off interface is an interface that is allocated on demand rather than upon COM class allocation.
●
●
Using custom interfaces in ATL Most COM objects support more than one interface; using ATL makes support for more than one interface an easy task. When to use ATL for COM server development You'll find situations where using other techniques, such as the MFC framework, provides more benefits than using ATL.
In the past, designing COM objects was relegated to the use of large application frameworks such as MFC or to building your own components. Using large application frameworks eases the construction and implementation of COM objects but requires a significant amount of overhead code for the building and distribution of the COM servers. Conversely, building your own COM server framework results in fast, lightweight objects but requires a significant amount of up-front programming to implement the control. To address this dilemma, Microsoft created the ActiveX Template Library (ATL), which provides a middle ground between large application frameworks and building your own COM objects. ATL is a set of template-based C++ classes that simplifies the programming of COM objects. ATL provides the necessary COM foundation, allowing the focus to be on programming the functionality of your objects. ATL is shipped with Visual C++ 5.0 and is backward compatible with Visual C++ 4.1 and 4.2. ATL can be downloaded separately from the Microsoft Web site at http://www.microsoft.com.
NOTE: Users of Visual C++ 4.0 must upgrade to version 4.1 or higher of the Visual C++ compiler in order to use the ActiveX Template Library.
The goal of ATL is to allow for the easy creation of small, fast COM servers. This goal has been achieved by the following: ●
Eliminating the need for static libraries (LIBs) or Dynamic Link Libraries (DLLs)
●
Eliminating the need for C runtime library start-up code
Static library and DLL dependencies are removed by providing all of the source code for the ATL libraries. The source code for ATL is a set of C++ class templates. The small set of ATL code gets compiled into the COM server during the building process. The overhead of ATL in an in-process COM server is less than 5K.
Reaping Benefits of the ActiveX Template Library The ActiveX Template Library is the first attempt to create a C++ framework with the sole intention of creating COM objects. Since COM object creation was the primary goal of the framework, ATL has been trimmed of unnecessary baggage such as bloated UI components. The use of ATL provides developers with a number of benefits including the following: ●
●
Support for multiple types of COM servers, including in-process servers, local servers, service servers, and DCOM servers Support for multiple-threading models, including the standard single-threading model, apartment-model threading, and free-threading
●
Various interface types, including custom COM interfaces, dual interfaces, tear-off interfaces, and IDispatch (OLE automation) interfaces
●
Enumeration support through the IEnumXXX interface
●
OLE error-handling through the IErrorInfo interface
Support for Multiple Server Types One of the biggest benefits of using ATL is the support for the creation of multiple COM server types. When using the ATL wizard, the shell classes for each server type are automatically created during project initialization. ATL provides support for the following types of COM servers. ●
●
●
●
In-process server--An in-process server is implemented as a DLL that exists and can be accessed only on the computer in which the server is installed. In-process servers are typically small and fast and are the most common type of COM server. An example of an in-process server is a spelling-checker COM object. Local server--A local server is implemented as an EXE. Like the in-process server, the local server can be accessed only on the computer in which the server is installed. The business charting application Visio is an example of a local server. Service server--A service server is implemented as an EXE and can be run only on the Windows NT operating system. Services are analogous to UNIX daemons, which are background tasks that are running but not directly controlled by the system user. A service is accessible to all users of the system and is started during system boot-up, as opposed to traditional programs, which are started only after a user logs on to the computer. A database server that supports COM is an example of a service server. Remote server--A remote server is implemented as an EXE and is accessible from remote computers using either DCOM or Remote Automation. Remote servers take advantage of the distributed computing by allowing the server to provide services to client applications not located on the same computer as the server. A database server is also a great example of a remote server.
Threading Models Supported by ATL ActiveX has different threading models that can be utilized by COM servers. The ATL library provides built-in support for these different types of threading. Each model provides different capabilities, and care must be taken when deciding which model will be supported by the COM server. ●
●
Single-threading model--This model is by far the most restrictive of all models supported by ActiveX. This model provides support for only one thread to create, use, and access OLE objects. This model is obsolete and is supported only because the original OLE architecture was implemented on the 16-bit Windows 3.x platform, which did not support multiple threads. This is the default threading model for COM servers. Apartment-model threading--This model fills the gap between the single-threaded model and the freethreaded model. While multiple threads can be utilized to access COM objects, care must be taken in how and when these objects are accessed. Each thread using COM objects must call the OLE initialization routines. In apartment-model threading, global variables accessed by each thread must be protected against simultaneous access.
●
Free-threading model--The free-threading model is by far the most flexible and unrestrictive. This model allows multiple threads to implement, access, and use COM objects. This mode of threading is supported only in the Windows NT 4.0 environment.
CAUTION: When implementing free-threading servers, the burden for protecting data within a COM class from simultaneous updates or access by multiple threads falls on the programmer. Multiple threads may be attempting to access local data within the same instance of a COM object. ATL does not provide built-in data access synchronization. The use of Win32 synchronization objects such as events, semaphores, mutexes, and critical sections is needed for protecting COM class data.
Tear-Off Interfaces A tear-off interface, a new concept introduced in the ATL framework, is an optimization of a regular COM interface in that it doesn't actually exist until it is instantiated by a call to QueryInterface on your object for that interface. Since the interface does not exist until asked for, it does not consume system memory resources. When the Release method is called on the interface and the reference count on that interface returns to zero, the interface is removed from memory. Typically, tear-off interfaces are used only for those interfaces that are expected to be used less often than others, such as ISupportErrorInfo.
NOTE: Tear-off interfaces should not be used for commonly used interfaces because the overhead of memory allocation and deallocation and memory fragmentation would outweigh the benefits of the interface.
To implement tear-off interfaces, declare a class that inherits from all the interfaces you want to implement in the tear-off, as well as from CComTearOffObjectBase, where Owner is the class of the main object. Then provide a normal BEGIN_COM_MAP...END_COM_MAP() specification of interfaces in the tearoff, and use the COM_INTERFACE_ENTRY_TEAR_OFF macro in the main object's COM map.
Implementing Interface Aggregation You can implement aggregation in ATL servers with very little work. Aggregation is when an object exposes another object's interface pointer as its own. For example, if an application has a pointer to interface A and needs to access interface B, and if interface A supports aggregation, the application can call QueryInterface on interface A to obtain interface B. The only penalty imposed for supporting aggregation is needing a somewhat larger server. The benefit is the flexibility to expose interfaces from objects contained in the server. In order to make a server aggregatable, use the macro DECLARE_AGGREGATABLE in the COM object's class. If aggregration is not desired, use the macro DECLARE_NOT_AGGREGATABLE to disable aggregration. By default, aggregration is supported by projects created with the ATL COM AppWizard.
Built-In Support for Error Handling
ATL supports the OLE error reporting mechanism with the Error() member function in the CComCoClass and CComISupportErrorInfoImpl classes. These classes each have a member, InterfaceSupportsErrorInfo(), that indicates whether returning rich error information is supported. By using this mechanism, custom COM interfaces can provide helpful information to the end user if error situations are encountered.
Creating a COM Server Using ATL When using the ActiveX Template Library, the creation of COM servers is a trivial task. The ATL installation creates an ATL COM AppWizard that can be accessed from the Visual C++ development environment. The ATL COM AppWizard, like the MFC AppWizard, presents the user with a step-by-step set of options for the creation of a COM server. The end result of running the wizard is a ready-to-be-built project with all necessary class template source code for the COM classes and interfaces that will be implemented within the project.
NOTE: In Chapter 12, an interface library entitled IFish was created. The IFish and IBass interfaces built in that example will be constructed using the ATL library. The project AtlCustomBass will create a COM class CAtlCustomBass, used for accessing the IFish interfaces. The CAtlCustomBass class will be implemented as an in-process server.
Using the ATL COM Wizard to Create a COM Server To get started using the ATL COM AppWizard, a new project must be created. A new project can be created by performing the following steps: 1. Select the New command from the File menu in the Visual C++ development environment. 2. From the New dialog, select the Projects tab. 3. From the Projects tab, select the ATL COM AppWizard (see fig. 13.1). FIG. 13.1 Select the ActiveX Template Library COM AppWizard to create an ATL-based COM server. 4. Enter the name of the project in the Name edit box. For this project, enter the name AtlCustomBass. Then click the OK button.
The ATL COM AppWizard is presented in Figure 13.2. FIG. 13.2 Choose the COM object options by using the ATL COM AppWizard. 5. Select the type of COM server to create. The AtlCustomBass project is created as a DLL. It is good practice to select the option "Allow merging of _proxy/stub code" (AtlCustomBass selects this option). The stub code option provides parameter marshalling for the objects. If your server needs to support MFC, select the option Support MFC. Click the Finish button to create the ATL COM server. A New Project Information dialog is displayed (see fig. 13.3). Click OK, and the project is automatically created.
FIG. 13.3 The New Project Information dialog box recaps the ATL COM server options. After creating the project, a COM object needs to be added to the project. To add a COM object, perform the following steps: 1. Select the New Class command from the Insert menu of the Visual C++ development environment. The New Class dialog is displayed, as shown in Figure 13.4. FIG. 13.4 Use the New Class dialog box to create COM classes. 2. Select ATL Class from the Class type drop-down list. Selecting ATL Class means that the new class will be derived from the ATL framework. 3. Type AtlCustomBass1 in the Name edit box. 4. Select the Custom Interface type radio button. There are two interface type choices: ❍
❍
Dual Interface--Derives the COM interfaces within the server from the IDispatch interface used for OLE automation. Custom Interface--Derives the COM interfaces from the IUnknown interface. If OLE automation is not needed, this is the recommended interface.
5. Specify 2 (two) in the Number of interfaces edit box. This setting determines how many interfaces the COM object will contain. The New Class dialog imposes a limit of 3. Again, this limit is imposed by the dialog, not the ATL library. The AtlCustomBass project will contain two interfaces (IFish and IBass) in the object. 6. Select the Edit button to change the default interface names. The Edit Interface Information dialog shown in Figure 13.5 is displayed. FIG. 13.5 The interface names can be changed from the Edit Interface Information dialog. 7. To change the names of the interfaces, click each interface shown in the Interface Names list. The CAtlCustonBass1 class supports the IFish and IBass interfaces. Enter IFish and IBass in the Interface Names list. Click the OK button when finished. 8. Click the OK button in the New Class dialog box to create the class. All the templates for the ATL COM server are now created. What remains is for the server's specific implementations to be incorporated. Before performing the customizations, you need to examine the results created by the ATL COM AppWizard.
Examining the Results of the ATL COM Wizard A total of 11 files were created when the ATL COM AppWizard and New Class dialogs were used to create the
ATLCustomBass project. Table 13.1 shows the filenames and purpose of each file created. Table 13.1 Files Created for the AtlCustomBass Project Filename
Purpose
Stdafx.cpp
Contains the includes needed globally for the project. This usually generates precompiled headings used by all other c or cpp files.
AtlCustomBass.cpp
Contains the COM server entry point implementations and COM class registration function.
AtlBass1.cpp
Skeleton cpp file for the AtlCustomBass COM object. All class and interface implementations are placed in this file.
AtlCustomBass.def
Export definition file for the COM object server.
AtlCustomBassps.def Export definition file for the interface library. The interface library is used for generating proxy code for parameter marshaling. AtlBass1.h
AtlCustomBass COM object definition file.
AtlCustomBass.idl
AtlCustomBass COM class and interface IDL definitions.
Resource.h
Standard resource file used for icons, version information, and so on.
Stdafx.h
Wrapper include file that includes all needed ATL header files.
Atlcustombassps.mk Makefile used for compiling the interface definition file. This file produces the parameter marshaling code for the COM interfaces.
As you can see, the AppWizard and New Class dialog have been busy on your behalf creating the templates necessary for creating COM objects. You now need to implement the actual interfaces.
Implementing the COM Server Access Functions Chapter 12 includes information about several access functions that must be exported from the COM server. These functions are accessed by the COM libraries to load, unload, and register the objects within the server. These functions are shown in the definition file for the CAtlCustomBass server (see Listing 13.1).
Listing 13.1 AtlCustomBass.def--Library Definition File for CAtlCustomBass ; AltCustomBass.def : Declares the module parameters. LIBRARY ALTCUSTOMBASS.DLL EXPORTS DllCanUnloadNow @1 PRIVATE DllGetClassObject @2 PRIVATE DllRegisterServer @3 PRIVATE DllUnregisterServer @4 PRIVATE The ATL library provides all of the necessary code for implementing the DLL access functions. For the CAtlCustomBass project, these functions are implemented in the file (see Listing 13.2).
Listing 13.2 AtlCustomBass.cpp--ATL Implementation of DLL Access Functions for COM Server
#include "stdafx.h" #include "resource.h" #include "initguid.h" #include "AltCustomBass.h" #include "AltCustomBass1.h" #include "dlldatax.h" #define IID_DEFINED #include "AltCustomBass_i.c" #ifdef _MERGE_PROXYSTUB extern "C" HINSTANCE hProxyDll; #endif CComModule _Module; BEGIN_OBJECT_MAP(ObjectMap) OBJECT_ENTRY(CLSID_CAltCustomBass1, CAltCustomBass1) END_OBJECT_MAP() ///////////////////////////////////////////////////////////////////////////// // DLL Entry Point extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { lpReserved; #ifdef _MERGE_PROXYSTUB if (!PrxDllMain(hInstance, dwReason, lpReserved)) return FALSE; #endif if (dwReason == DLL_PROCESS_ATTACH) { _Module.Init(ObjectMap, hInstance); DisableThreadLibraryCalls(hInstance); } else if (dwReason == DLL_PROCESS_DETACH) _Module.Term(); return TRUE; // ok } ///////////////////////////////////////////////////////////////////////////// // Used to determine whether the DLL can be unloaded by OLE STDAPI DllCanUnloadNow(void) { #ifdef _MERGE_PROXYSTUB if (PrxDllCanUnloadNow() != S_OK) return S_FALSE; #endif return (_Module.GetLockCount()==0) ? S_OK : S_FALSE; } ///////////////////////////////////////////////////////////////////////////// // Returns a class factory to create an object of the requested type STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { #ifdef _MERGE_PROXYSTUB if (PrxDllGetClassObject(rclsid, riid, ppv) == S_OK) return S_OK;
#endif return _Module.GetClassObject(rclsid, riid, ppv); } ///////////////////////////////////////////////////////////////////////////// // DllRegisterServer - Adds entries to the system registry STDAPI DllRegisterServer(void) { #ifdef _MERGE_PROXYSTUB HRESULT hRes = PrxDllRegisterServer(); if (FAILED(hRes)) return hRes; #endif // registers object, typelib and all interfaces in typelib return _Module.RegisterServer(TRUE); } ///////////////////////////////////////////////////////////////////////////// // DllUnregisterServer - Removes entries from the system registry STDAPI DllUnregisterServer(void) { #ifdef _MERGE_PROXYSTUB PrxDllUnregisterServer(); #endif _Module.UnregisterServer(); return S_OK; }
Using IDL to Create Object Definitions All COM classes and interfaces are defined through IDL, the Interface Definition Language. IDL is also used in the creation of RPC interfaces and Automation interfaces. When the AtlCustomBass project was created, the COM AppWizard generated the file AtlCustomBass.idl. This file contains template structures for the COM class and interfaces specified when running the AppWizard. All methods for the custom interfaces must be added to this file. Listing 13.3 illustrates the contents of AtlCustomBass.idl with the methods for custom interface IFish and IBass.
Listing 13.3 AtlCustomBass.idl--Adding Custom Interface Methods to the Project Interface Definition File import "unknwn.idl"; #define MAX_FISH_BSTR_LEN 255 typedef [string] WCHAR FISH_BSTR[MAX_FISH_BSTR_LEN]; // This file will be processed by the MIDL tool to // produce the type library (AltCustomBass.tlb) and marshalling code. [ object, uuid(F3C2DDA2-7434-11D0-B6FC-00008607092E), helpstring("IFish Interface"), pointer_default(unique) ] interface IFish : IUnknown { import "oaidl.idl";
HRESULT IsFreshwater([out] BOOL *pBool); HRESULT GetFishName([out, string] FISH_BSTR p); }; [ object, uuid(F3C2DDA3-7434-11D0-B6FC-00008607092E), helpstring("IFish Interface"), pointer_default(unique) ] interface IBass : IUnknown { import "oaidl.idl"; HRESULT GetLocation([out, string] FISH_BSTR p); HRESULT SetLocation([in, string] FISH_BSTR p); HRESULT EatsOtherFish([out] BOOL *pBool); }; [ uuid(F3C2DDA0-7434-11D0-B6FC-00008607092E), version(1.0), helpstring("AltCustomBass 1.0 Type Library") ] library ALTCUSTOMBASSLib { importlib("stdole32.tlb"); [ uuid(F3C2DDA6-7434-11D0-B6FC-00008607092E), helpstring("AltCustomBass1 Class") ] coclass CAltCustomBass1 { [default] interface IFish; interface IBass; }; }; In the IDL file for AtlCustomBass are definitions for two interfaces, IFish and IBass, and the COM class, CAltBass1. As you can see from Listing 13.3, both the IFish and IBass interfaces are derived from the IUnknown interface. In this example, a new type has been defined, and that type is FISH_BSTR. FISH_BSTR is defined as a wide-character string that is 255 characters in length. The wide-character string is used so that the server can be compiled as either multibyte (default) or UNICODE. The maximum length must be specified in the IDL definition. This is a requirement of the MIDL compiler used to compile the IDL file. In order for the MIDL compiler to produce code that handles parameter marshaling, the absolute length of the data item passed into functions must be known. The COM class CAltCustomBass1 is a COM Object library that acts as a container for the COM interfaces IFish and IBass. The specified coclass identifies the definition as a COM class. As you can see from the definition, the IFish interface is the default interface. This is the pointer that is returned when the IID_IUnknown interface is queried. The IDL file is compiled separately from the AtlCustomBass project. When the AtlCustomBass project is built, the AtlCustomBass.idl file is compiled with the MIDL compiler first. This step is automatically done as part of the build process. The IDL can also be compiled separately outside the IDE. The ATL COM AppWizard generates a separate
makefile for the IDL file (AtlCustomBassps.mk). The makefile program nmake can be run from a command prompt to compile the IDL file. The command line would look like this: nmake -fatlcustombassps.mk
Implementing the COM Interface Implementing the COM interface using the ATL library is a simple process. For the programmer familiar with C++, this implementation is as simple as adding the interface methods to the COM class. The ATL library takes care of handling all of the IUnknown and IClassFactory interfaces, thus removing this burden from the developer. Even the MFC framework requires that the IUnknown interface must be handled by the programmer. Listing 13.4 shows the COM class CAtlCustomBass definition file (AtlCustomBass.h). The only modifications needed for the AtlCustomBass COM server is to add the public interface methods and the variables needed by the class.
Listing 13.4 AtlCustomBass.h--C++ Class Definition for the AtlCustomBass COM Server class CAltCustomBass1 : public IFish, public IBass, public CComObjectRoot, public CComCoClass { public: CAltCustomBass1(); BEGIN_COM_MAP(CAltCustomBass1) COM_INTERFACE_ENTRY(IFish) COM_INTERFACE_ENTRY(IBass) END_COM_MAP() //DECLARE_NOT_AGGREGATABLE(CAltCustomBass1) // Remove the comment from the line above if you don't want your object to // support aggregation. The default is to support it DECLARE_REGISTRY(CAltCustomBass1, _T("AltCustomBass.AltCustomBass1.1"), _T("AltCustomBass.AltCustomBass1"), IDS_ALTCUSTOMBASS1_DESC, THREADFLAGS_BOTH) // IFish STDMETHOD(GetFishName)(FISH_BSTR pStr); STDMETHOD(IsFreshwater)(BOOL* pBool); // IBass STDMETHOD(GetLocation)(FISH_BSTR p); STDMETHOD(SetLocation)(FISH_BSTR p); STDMETHOD(EatsOtherFish)(BOOL *pBool); public: WCHAR m_FishName[255]; BOOL m_bFreshWater; WCHAR m_Location[255]; BOOL m_bEatsFish; };
To support the IFish and IBass interfaces, the custom methods for each interface have been defined as standard C++ methods. The ATL Template Library was developed to directly support and work with COM. This is in contrast to the MFC framework, which does not directly use COM but has hooks to manipulate COM. The COM class CAltCustomBass is derived from the classes and interfaces shown in Table 13.2. Table 13.2 Base Classes and Interfaces for CAtlCustomBass1 Class
Description
IFish
IFish COM interface, which is one of the custom interfaces supported by the class
IBass
Another custom interface specified during class construction
CComObjectRoot ATL class that implements all reference counting and thread model-specific implementations CComCoClass
ATL class that implements the class factory for the object, aggregation, and error handling
The ATL library supports multiple interfaces through inheritance rather than through nested classes. MFC uses nested classes for the support of multiple interfaces. For a more thorough discussion of techniques used for supporting multiple interfaces, refer to Chapter 14, which demonstrates the difference between direct inheritance and nested classes. Implementing the C++ method that will support the COM interface is a straightforward matter. Listing 13.5 shows the implementation of the IFish and IBass COM interfaces used in the CAltCustomBass1 class.
Listing 13.5 CAltCustomBass1.cpp--Listing of CAltCustomBass1.cpp, Which Implements the COM Interfaces for IFish and IBass CAltCustomBass1::CAltCustomBass1(void) { wcscpy( m_FishName, L"Large Mouth Bass"); wcscpy( m_Location, L"Under Lily Pads"); m_bEatsFish = TRUE; m_bFreshWater = TRUE; } STDMETHODIMP CAltCustomBass1::GetFishName( FISH_BSTR pStr) { // TRACE("CAltCustomBass1::GetFishName\n"); if (pStr) wcscpy(pStr, m_FishName); return (HRESULT)NOERROR; } STDMETHODIMP CAltCustomBass1::IsFreshwater( BOOL *pBool ) { // TRACE("CAltCustomBass1::IsFreshwater\n"); if (pBool) { *pBool = m_bFreshWater; return S_OK;
} return (HRESULT)NOERROR; } // CBass:Fish implementation of IFish STDMETHODIMP CAltCustomBass1::GetLocation( FISH_BSTR pStr) { // TRACE("CAltCustomBass1::GetLocation\n"); if (pStr) wcscpy(pStr, m_Location); return (HRESULT)NOERROR; } STDMETHODIMP CAltCustomBass1::SetLocation( FISH_BSTR pStr) { // TRACE("CAltCustomBass1::SetLocation\n"); if (pStr) wcscpy(m_Location, pStr); return (HRESULT)NOERROR; } STDMETHODIMP CAltCustomBass1::EatsOtherFish( BOOL *pBool ) { // TRACE("CAltCustomBass1::EatsOtherFish\n"); if (pBool) { *pBool = m_bEatsFish; return S_OK; } // return E_BADPOINTER; return (HRESULT)NOERROR; } Again, note that the ATL Template Library implements the IUnknown interface for you, resulting in much fewer coding requirements for the developer. Now that the method implementation is complete, the COM server can be compiled and built.
Using Object Maps to Specify COM Objects The ATL library uses object maps to specify the COM objects that make up a particular ATL server. Object maps are arrays of structures that tell ATL about the objects implemented in a server. The members of an object map include the CLSID of the object and the class of the object. When a specific interface is requested through the QueryInterface method, ATL uses COM maps to map interface IDs (IIDs) to offsets in the interface. COM maps are used by the class CComObjectRoot. When a user calls QueryInterface(), the ATL library internally calls InternalQueryInterface() to return an interface pointer based on an IID passed in. Thirteen different types of entries can reside in a COM map (see Table 13.3). Table 13.3 Types of Entries COM Entry Type
Description
COM_INTERFACE_ENTRY
Interface where only the class name needs to be in. The IID is synthesized by prepending IID_ to the class name. This is the basic and popular form of COM interfaces.
COM_INTERFACE_ENTRY_IID
Interface where the IID and the class name need to be passed in, for example, COM_INTERFACE_ENTRY_IID (IID_IFish, IFish).
COM_INTERFACE_ENTRY2
Interface where it is necessary to distinguish conflicting interfaces. For example, if you have dual interfaces (IFoo and IBar) in an object, specifying COM_INTERFACE_ENTRY (IDispatch) would be ambiguous because both IFoo and IBar derive from Idispatch. However, by specifying COM_INTERFACE_ENTRY2 (IDispatch, IFoo), you can control which interface is returned.
COM INTERFACE_ENTRY2_IID
Interface where the IID and the class name need to be passed in and you need to disambiguate interfaces.
COM_INTERFACE_ENTRY_TEAR_OFF
Interface is a tear-off interface. Tear-off interfaces are generally created each time a client calls QueryInterface for a particular interface, even if a tear-off for that interface is already instantiated.
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF
Interface is a tear-off interface; however, ATL creates an object implementing the tear-off interface only the first time the interface is requested. Subsequent QueryInterface calls will reuse the object previously instantiated.
COM_INTERFACE_ENTRY_AGGREGATE
Indicates that a QueryInterface call for a particular interface should go through the aggregate.
COM_INTERFACE_ENTRY_AGGREGATE
Indicates that a QueryInterface call for an interface _BLIND should be blindly forwarded to the aggregate.
COM_INTERFACE_ENTRY_AUTOAGGREGATE
Automatically creates the aggregate when a client performs a QueryInterface for a particular interface on the aggregate.
COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND Automatically creates the aggregate when a client calls QueryInterface for an interface that cannot be found on the outer object. COM_INTERFACE_ENTRY_CHAIN
Chains to the COM map of a base class.
COM_INTERFACE_ENTRY_FUNC
Allows you to programmatically hook the creation of a particular interface pointer.
COM_INTERFACE_ENTRY_FUNC_BLIND
Allows you to programmatically hook the creation of a pointer to any interface, thus not found.
When to Use the ActiveX Template Librarys
The ActiveX template library offers many advantages to builders of COM interfaces and COM objects. The ATL library offers the advantage of building fast, lightweight COM servers. However, using ATL may not be the best method of implementation depending on the situation. ATL is focused entirely on the creation of small, fast COM servers in C++. ATL is optimized for the creation of objects that expose custom or dual-interfaces and has absolutely no inherent support for more complex COMbased architectures, such as ActiveX documents. If you want to create generic COM objects or OLE automation objects with dual-interface support or you want to support COM's free-threading model (available with Windows NT 4.0 and later versions of Windows) and you don't have a significant user interface in your object, ATL is the choice for producing the smallest, fastest code. If you want to create complex servers that need to support user-interface items, ActiveX controls, or ActiveX documents, then a more robust framework such as MFC should be used.
From Here... This chapter has illustrated some of the many benefits gained when using the ActiveX Template Library. The ATL COM AppWizard was used to create a framework for a COM server. The New Class dialog was used to create a COM object with multiple custom interfaces. All that the user must implement is the custom functionality of the server. Chapter 12 uses the MFC framework to create COM servers. MFC is a feature-rich application framework that can be used for building COM servers. Chapter 14 illustrates a custom COM architecture for building COM servers. The custom architecture is not derived from ATL or MFC.
Chapter 14 Creating ActiveX COM Objects and Custom Interfaces on Your Own ●
Creating ActiveX COM Objects and Custom Interfaces on Your Own ❍ Creating a Basic In-Process Server ■ Creating the Project Definition File ■ Listing 14.1 CUSTOMBASS.DEF--DLL Library Definition File for CUSTOMBASS.DLL ■ Custom COM Server Architecture ❍ Creating the COM Class COBass ■ Listing 14.2 COBASS.H--COM Object COBass that Implements the IFish and IBass Interfaces ■ Listing 14.3 COBASS.CPP--IUnknown Implementation for COBass ■ Listing 14.4 COBASS.CPP--COBass Interface Implementations of the IFish and IBass Interfaces ■ Listing 14.5 CUSTOMBASSID.H--Header File CUSTOMBASSID.H, which Contains the Implementation of CLSID for the COBass Class ❍ Implementing the COBass Class Factory ■ Listing 14.6 FACTORY.H--Class Factory Definition File, FACTORY.H, for CFBass ■ Listing 14.7 FACTORY.CPP--Implementation of the CFBass Class and IUnknown Interface ■ Listing 14.8 FACTORY.CPP--IUnknown and IClassFactory Implementations in CImpIClassFactory ❍ Implementation of the Server Application ■ Listing 14.9 SERVER.H--CServer Class Definition ■ Listing 14.10 SERVER.CPP--CServer Object Implementation ❍ Implementation of the Server Access Functions ■ Listing 14.11 CUSTOMBASS.CPP--Server Access Function Implementation ❍ Compiling and Testing the COM Server ❍ From Here...
Creating ActiveX COM Objects and Custom Interfaces on Your Own
●
●
●
●
●
Writing a custom handler for registering components for regsvr32.exe The server can directly register all of the COM Objects that it supports. Implementing a custom class factory (IClassFactory) interface Without the benefit of a COM framework, you must implement an IClassFactory interface for each COM Object created. Building a DLL-based server for containing COM Objects Building COM servers without a framework requires implementing some basic COM APIs that are entry points into DLL-based servers. Unloading DLLs from memory when instances of COM Objects are no longer being used COM servers are responsible for removal of all COM components that are no longer being used. Utilizing different strategies for implementing COM interfaces Multiple techniques can be used for implementing COM Objects.
A variety of methods are available for creating COM classes based upon the MFC and ActiveX frameworks. In this chapter, you will examine methods for creating COM classes without MFC or ActiveX frameworks. How the COM class was defined or implemented does not matter to the client application using the class. The use and function of the class are identical to the client. Defining and implementing COM classes without a framework such as MFC or ActiveX is an easy task, although additional work must be performed by the COM developer for object creation and termination. Although application frameworks tend to make the creation of COM components easier, you will find several advantages to creating your own COM classes: ●
●
●
The component does not have to carry excess baggage (code) incurred from using a framework. When using C++ without a framework, a particular base class is not required from which to derive COM classes. This removes excess baggage which is not needed from the implementation of the class. COM components not derived from a framework class can still be used and accessed from within a framework-based DLL or EXE.
Creating COM components without the benefit of a framework also has some disadvantages. The biggest drawback is that you need to perform all of the detailed work such as creating IClassFactory interfaces, which are normally supplied by the framework.
Creating a Basic In-Process Server In Chapter 12, two COM interfaces are defined: IFish and IBass. These interfaces are defined within a project called IFISH.DLL. The IFish and IBass interfaces are accessed through a COM class entitled CBass, which is derived from the MFC base class CCmdTarget. In this chapter, the interfaces IFish and IBass will again be used along with a new class entitled COBass, which is used to access the interfaces. The COBass class will be implemented within a DLL and will not be derived from a framework class. To create the basic in-process server, the New dialog will be used. To create the CUSTOMBASS project, perform the following steps: 1. From within the Visual C++ development environment, select the command New from the File menu. 2. Select Projects tab from the New dialog. 3. From the Projects tab, select Win32 Dynamic Link Library. Enter the project name CUSTOMBASS into the Project name edit box. Select the OK button. 4. The project CUSTOMBASS is now created.
Creating the Project Definition File When creating a generic dynamic link library (DLL), the New dialog does not create a definition file (DEF). All DLLs must have a definition file that is used to define information about the project, such as functions that are exported from the library. All DLLs that support COM classes must export some basic functions in order for the client applications to access the COM classes. From within the Visual C++ Developer Studio, create a file called CUSTOMBASS.DEF. Listing 14.1 illustrates the contents of this file.
Listing 14.1 CUSTOMBASS.DEF--DLL Library Definition File for CUSTOMBASS.DLL LIBRARY CUSTOMBASS DESCRIPTION `CUSTOMBASS Dynamic Link Library' EXPORTS DllGetClassObject DllCanUnloadNow DllRegisterServer DllUnregisterServer
The function DllGetClassObject is called to create an instance of a COM Object. DLLCanUnloadNow is called periodically by the operating system to remove unused DLLs from system memory. DllRegisterServer is used by the program regsvr32.exe to allow the DLL to register all COM Objects with the Windows Registry. The function DllUnregisterServer is called to remove all registry settings for the COM Objects from the Windows Registry.
Custom COM Server Architecture When creating a COM server without the aid of an application framework such as MFC, you need to establish a system architecture for implementing the COM model. The architecture used in the CUSTOMBASS project consists of three classes: ●
●
●
CServer: A global class within CUSTOMBASS.DLL that acts as a reference manager for all COM Objects within the server. This class is responsible for unloading the DLL from memory and providing reference bookkeeping. CFBass: A class factory for the COBass COM Object. This class implements the IClassFactory interface for the server. COBass: The COM Object class that contains and implements the IFish and IBass interfaces.
NOTE: While the CUSTOMBASS project utilizes three classes as its basic architecture, many different approaches can be used. The only requirement is that in all COM architectures there must be a COM Object class and a class factory for each COM class. When implementing COM Objects without the use of a framework, the interfaces supported by the class can be implemented through several methods: ●
●
Single inheritance: In this scheme, each interface is implemented with a single C++ implementation class. One controlling object then contains pointers to all of the implementation classes. Although this mechanism works, it requires a significant amount of coding, and each additional interface supported by the COM class requires reprogramming of the COM Object. Nested interface classes: This technique is similar to single inheritance, but instead of having a separate C++ class for each interface, the interface implementation classes are contained or nested within the COM Object. When a COM Object is created, it also creates each of the interface classes. This technique is the one that MFC uses for interface implementation within a class. Although this technique requires less code than the single inheritance technique, it still requires a fair amount of coding to implement. CFBass, the class factory in CUSTOMBASS,
uses this technique. ●
Multiple inheritance: This method is by far the easiest one for implementing and coding. The multiple inheritance technique requires that the COM Object class be derived from the interface classes. The ATL framework utilizes this method. This technique requires less code and consumes much less memory at runtime. The COM class COBass supports the IFish and IBass interfaces through multiple inheritance.
Creating the COM Class COBass In the CUSTOMBASS project, the COM class COBass, which will implement the IFish and IBass COM interfaces, is created. You may want to refer to Chapter 12, where the IFish and IBass interfaces are implemented in the MFC-derived COM class CBass. The class COBass is derived directly from the IFish and IBass interfaces. By definition, this makes the interface methods of IFish, IBass, and IUnknown an integral part of COBass. The class Cbass is derived from the CCmdTarget class. The IFish and IBass interfaces are added as members of the COBass class (nested interfaces). COBass is a C++ class that is derived from the IBass and IFish interfaces. Listing 14.2 shows the class definition for COBass. This is different from CBass, which was derived from the MFC class CCmdTarget.
Listing 14.2 COBASS.H--COM Object COBass that Implements the IFish and IBass Interfaces #if !defined(COBASS_H) #define COBASS_H #ifdef __cplusplus #include "..\ifish\ifish.h" #include "..\ifish\ibass.h" class COBass : public IFish , public IBass { public: // Main Object Constructor & Destructor. COBass(IUnknown* pUnkOuter, CServer* pServer); ~COBass(void); // shared IUnknown methods. Main object, non-delegating. STDMETHODIMP QueryInterface(REFIID, PPVOID); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IFish methods STDMETHODIMP IsFreshwater(BOOL *); STDMETHODIMP GetFishName( LPTSTR );
// IBass methods STDMETHODIMP GetLocation( LPTSTR ); STDMETHODIMP SetLocation( LPTSTR ); STDMETHODIMP EatsOtherFish( BOOL *); private: // We declare nested class interface implementations here. // Main Object reference count. ULONG m_cRefs; // Outer unknown (aggregation & delegation). IUnknown* m_pUnkOuter; // Pointer to this component server's control object. CServer* m_pServer; char m_zFishName[256]; char m_zLocation[256]; BOOL m_bEatsOtherFish; BOOL m_bFreshwater; }; typedef COBass* PCOBass; #endif // __cplusplus #endif // COBASS_H When deriving COM classes from multiple interfaces, less coding is needed to implement the COM Object and the interfaces. One of the advantages of deriving a COM class from multiple interfaces is that only one implementation of the IUnknown interfaces is required. The delegation of the IUnknown interface is also avoided. Listing 14.3 illustrates the implementation of the IUnknown interface in the COBass class.
Listing 14.3 COBASS.CPP--IUnknown Implementation for COBass #include #include #include "comutil.h" #include "..\ifish\ifish.h" #include "..\ifish\ibass.h" #include "custombassid.h" #include "server.h" #include "cobass.h" COBass::COBass( IUnknown* pUnkOuter, CServer* pServer) { // Zero the COM object's reference count. m_cRefs = 0; // No AddRef necessary if non-NULL, as we're nested. m_pUnkOuter = pUnkOuter;
// Assign the pointer to the server control object. m_pServer = pServer; lstrcpy (m_zFishName, "Bass"); lstrcpy (m_zLocation, "Lilly Pads"); m_bEatsOtherFish = TRUE; m_bFreshwater = TRUE; } COBass::~COBass(void) { } STDMETHODIMP COBass::QueryInterface( REFIID riid, PPVOID ppv) { HRESULT hr = ResultFromScode(E_NOINTERFACE); *ppv = NULL; if (IID_IUnknown == riid || IID_IFish == riid) { *ppv = (IFish *)this; } else if (IID_IBass == riid) { *ppv = (IBass *)this; } if (NULL != *ppv) { // We've handed out a pointer to the interface so obey the COM rules // and AddRef the reference count. ((LPUNKNOWN)*ppv)->AddRef(); hr = NOERROR; } return (hr); }
CAUTION: One important fact to remember is that this pointer is not a direct object pointer to any interface, not even to the IUnknown interface. This fact means that the interface pointer returned must be explicitly type-cast to the correct pointer for each interface. Type-casting the pointer changes the pointer value. C++ overloads the type-cast operators to allocate the right vtable for the interface type. Failure to not type-cast to the explicit interface yields unpredictable results.
In looking at the QueryInterface implementation, the IFish interface pointer is returned for the
interface IID's IFish and IUnknown. Since the IFish interface is derived from Iunknown, it is perfectly valid to return the IFish interface when the IUnknown interface is asked for. IFish is a matter of preference; the IBass interface can be chosen as well. The other COM interfaces are implemented as methods of the COBass class. Listing 14.4 illustrates the COM interface implementations.
Listing 14.4 COBASS.CPP--COBass Interface Implementations of the IFish and IBass Interfaces STDMETHODIMP_(ULONG) COBass::AddRef(void) { m_cRefs++; return m_cRefs; } STDMETHODIMP_(ULONG) COBass::Release(void) { m_cRefs--; if (0 == m_cRefs) { // We've reached a zero reference count for this COM object. // So we tell the server housing to decrement its global object // count so that the server will be unloaded if appropriate. if (NULL != m_pServer) m_pServer->ObjectsDown(); delete this; } return m_cRefs; } STDMETHODIMP COBass::GetFishName( char *pStr) { LOG("COBass::GetFishName\n"); if (pStr) lstrcpy((char *)pStr, m_zFishName); return (HRESULT)NOERROR; } STDMETHODIMP COBass::IsFreshwater( BOOL *pBool ) { LOG("COBass::IsFreshwater\n"); if (pBool) { *pBool = m_bFreshwater; return S_OK; } return (HRESULT)NOERROR; } STDMETHODIMP COBass::GetLocation( char *pStr)
{ LOG("COBass::GetLocation\n"); if (pStr) strcpy((char *)pStr, (LPCTSTR)m_zLocation); return (HRESULT)NOERROR; } STDMETHODIMP COBass::SetLocation( char *pStr) { LOG("COBass::SetLocation\n"); if (pStr) strcpy(m_zLocation, (char *)pStr); return (HRESULT)NOERROR; } STDMETHODIMP COBass::EatsOtherFish( BOOL *pBool ) { LOG("COBass::EatsOtherFish\n"); if (pBool) { *pBool = m_bEatsOtherFish; return S_OK; } return (HRESULT)NOERROR; } As shown in Listing 14.4, the interface implementations are straightforward C++ method implementations. Each method for the IFish and IBass interfaces are implemented as a method of the COBass class. Unlike the MFC-based Cbass implementation, there is no need to specify a specific interface for each method since the COBass class is derived directly from the IFish and IBass interfaces. One final note on the COBass interface implementations concerns the AddRef and Release methods implemented for the IUnknown interface. The AddRef method simply increments a counter within the object, indicating that another user is referencing the object. When Release is called three actions are taken: ●
●
●
The internal reference count is decremented. If the reference count is zero, the main application server object is signaled to decrement the global object counter. If the internal reference count is zero, the object deletes itself; although this procedure may seem unusual, it follows the COM rules that the object be responsible for its own termination when it is no longer referenced.
The last piece of the COBass class is the unique CLSID. To create the CLSID, run the tool GUIDGEN. Once the CLSID is created, it must be placed in a header file that acts as a define for the class implementation. For the COBass object, the file CUSTOMBASSID.H is created. The CLSID is then pasted into the file and added to the macro DEFINE_GUID (see Listing 14.5).
Listing 14.5 CUSTOMBASSID.H--Header File CUSTOMBASSID.H, which Contains the Implementation of CLSID for the COBass Class #ifndef _CLSID_CustomBass #define _CLSID_CustomBass //{A1C19FC0-66D6-11d0-ABE6-D07900C10000} DEFINE_GUID(CLSID_CustomBass,0xA1C19FC0, 0x66D6,0x11d0,0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00); #endif The macro DEFINE_GUID assigns the name CLSID_CustomBass to the class ID that was created via GUIDGEN. This macro is placed in a header file that is used by all clients that need to invoke an instance of CLSID_CustomBass. This file is not used by the server that implements the COM Object.
Implementing the COBass Class Factory Now that the COM class COBass is implemented, a class factory that creates the object must be implemented. A class factory is a COM interface (IClassFactory) that is responsible for creating instances of COBass. When using a framework such as MFC, the class factory for an object is implemented through the OLE_CREATE macro. Without the benefit of a framework, this interface must be implemented by the developer of the COM Object server. The COBass class factory is implemented in the class CFBass (see Listing 14.6).
Listing 14.6 FACTORY.H--Class Factory Definition File, FACTORY.H, for CFBass class CFBass : public IUnknown { public: // Main Object Constructor & Destructor. CFBass(IUnknown* pUnkOuter, CServer* pServer); ~CFBass(void); // IUnknown methods. Main object, non-delegating. STDMETHODIMP QueryInterface(REFIID, PPVOID); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void);
private: // We declare nested class interface implementations here. // We implement the IClassFactory interface (ofcourse) in this class // factory COM object class. class CImpIClassFactory : public IClassFactory { public: // Interface Implementation Constructor & Destructor. CImpIClassFactory( CFBass* pBackObj, IUnknown* pUnkOuter, CServer* pServer); ~CImpIClassFactory(void); // IUnknown methods. STDMETHODIMP QueryInterface(REFIID, PPVOID); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IClassFactory methods. STDMETHODIMP CreateInstance(IUnknown*, REFIID, PPVOID); STDMETHODIMP LockServer(BOOL); private: // Data private to this interface implementation of IClassFactory. ULONG m_cRefI; // Interface Ref Count (for debugging). CFBass* m_pBackObj; // Parent Object back pointer. IUnknown* m_pUnkOuter; // Outer unknown for Delegation. CServer* m_pServer; // Server's control object. }; // Make the otherwise private and nested IClassFactory interface // implementation a friend to COM object instantiations of this // selfsame CFBass COM object class. friend CImpIClassFactory; // Private data of CFBass COM objects. // Nested IClassFactory implementation instantiation. CImpIClassFactory m_ImpIClassFactory; // Main Object reference count. ULONG m_cRefs; // Outer unknown (aggregation & delegation). Used when this // CFBass object is being aggregated. Otherwise it is used // for delegation if this object is reused via containment. IUnknown* m_pUnkOuter; // Pointer to this component server's control object. CServer* m_pServer; }; typedef CFBass* PCFBass; The class CFBass uses the technique of nested interfaces to implement the IUnknown and the
IClassFactory interfaces. With nested interfaces, the second class definition, in this case CImpIClassFactory, is defined within the definition of the class CFBass. This technique allows the CImpIClassFactory object to be created when the constructor of CFBass is called. Each class implements its own interface. When the CFBass class constructor is called, the constructor for the class member m_ImpIClassFactory, which is of class CImpIClassFactory, is also called. Listing 14.7 illustrates the CFBass constructor as well as the IUnknown interfaces implemented in CFBass. Notice that if the IUnknown interface is called with an interface ID of IID_IClassFactory, the address of member CImpIClassFactory is returned.
Listing 14.7 FACTORY.CPP--Implementation of the CFBass Class and IUnknown Interface #include #include #include "comutil.h" #include "server.h" #include "factory.h" #include "cobass.h" CFBass::CFBass( IUnknown* pUnkOuter, CServer* pServer) : m_ImpIClassFactory(this, pUnkOuter, pServer) { // Zero the COM object's reference count. m_cRefs = 0; // No AddRef necessary if non-NULL, as we're nested. m_pUnkOuter = pUnkOuter; // Init the pointer to the server control object. m_pServer = pServer; } CFBass::~CFBass(void) { return; } STDMETHODIMP CFBass::QueryInterface( REFIID riid, PPVOID ppv) { HRESULT hr = E_NOINTERFACE; *ppv = NULL; if (IID_IUnknown == riid) { *ppv = this;
LOG("S: CFBass::QueryInterface. `this' pIUnknown returned."); } else if (IID_IClassFactory == riid) { *ppv = &m_ImpIClassFactory; LOG("S: CFBass::QueryInterface. pIClassFactory returned."); } if (NULL != *ppv) { // We've handed out a pointer to the interface so obey the COM rules // and AddRef the reference count. ((LPUNKNOWN)*ppv)->AddRef(); hr = NOERROR; } return (hr); } STDMETHODIMP_(ULONG) CFBass::AddRef(void) { m_cRefs++; LOGF1("S: CFBass::AddRef. New cRefs=%i.", m_cRefs); return m_cRefs; } STDMETHODIMP_(ULONG) CFBass::Release(void) { m_cRefs--; LOGF1("S: CFBass::Release. New cRefs=%i.", m_cRefs); if (0 == m_cRefs) { if (NULL != m_pServer) m_pServer->ObjectsDown(); delete this; } return m_cRefs; } The IClassFactory interface is analogous to the C++ new operator. IClassFactory is an interface used for creating COM classes. Each COM class is created through an IClassFactory interface. The IClassFactory interface is derived from IUnknown and contains the following methods:
// IClassFactory methods. STDMETHODIMP CreateInstance(IUnknown*, REFIID, PPVOID); STDMETHODIMP LockServer(BOOL); The method CreateInstance is used to create an instance of a COM class. LockServer
method increments or decrements a reference count within the COM server. If the count is greater than 0, the server cannot be removed from memory. Within the CFBass class, the IClassFactory is implemented through the class CImpIClassFactory. The IUnknown and IClassFactory implementations of CImpIClassFactory are shown in Listing 14.8.
Listing 14.8 FACTORY.CPP--IUnknown and IClassFactory Implementations in CImpIClassFactory CFBass::CImpIClassFactory::CImpIClassFactory( CFBass* pBackObj, IUnknown* pUnkOuter, CServer* pServer) { // Init the Interface Ref Count (used for debugging only). m_cRefI = 0; // Init the Back Object Pointer to point to the parent object. m_pBackObj = pBackObj; // Init the pointer to the server control object. m_pServer = pServer; // Init the CImpIClassFactory interface's //delegating Unknown pointer. // We use the Back Object pointer for // IUnknown delegation here if we are // not being aggregated. If we are being // aggregated we use the supplied // pUnkOuter for IUnknown delegation. // In either case the pointer // assignment requires no AddRef // because the CImpIClassFactory lifetime is // quaranteed by the lifetime of the parent object in which // CImpIClassFactory is nested. if (NULL == pUnkOuter) { m_pUnkOuter = pBackObj; LOG("S: CFBass::CImpIClassFactory Constructor. Non-Aggregating."); } else { m_pUnkOuter = pUnkOuter; LOG("S: CFBass::CImpIClassFactory Constructor. Aggregating."); } return; }
CFBass::CImpIClassFactory::~CImpIClassFactory(void) { LOG("S: CFBass::CImpIClassFactory Destructor."); return; } STDMETHODIMP CFBass::CImpIClassFactory::QueryInterface( REFIID riid, PPVOID ppv) { LOG("S: CFBass::CImpIClassFactory::QueryInterface. Delegating."); // Delegate this call to the outer object's QueryInterface. return m_pUnkOuter->QueryInterface(riid, ppv); } STDMETHODIMP_(ULONG) CFBass::CImpIClassFactory::AddRef(void) { // Increment the Interface Reference Count. ++m_cRefI; LOGF1("S: CFBass::CImpIClassFactory::Addref. Delegating. New cI=%i.",m_cRefI); // Delegate this call to the outer object's AddRef. return m_pUnkOuter->AddRef(); } STDMETHODIMP_(ULONG) CFBass::CImpIClassFactory::Release(void) { // Decrement the Interface Reference Count. --m_cRefI; LOGF1("S: CFBass::CImpIClassFactory::Release. Delegating. New cI=%i.",m_cRefI); // Delegate this call to the outer object's Release. return m_pUnkOuter->Release(); } STDMETHODIMP CFBass::CImpIClassFactory::CreateInstance( IUnknown* pUnkOuter, REFIID riid, PPVOID ppv) { HRESULT hr = E_FAIL; COBass* pCob = NULL; LOGF1("S: CFBass::CImpIClassFactory::CreateInstance. pUnkOuter=0x%X.",pUnkOuter); // NULL the output pointer. *ppv = NULL; // If the creation call is requesting aggregation // (pUnkOuter != NULL), // the COM rules state the IUnknown interface
// MUST also be concomitantly // be requested. If it is not so requested // (riid != IID_IUnknown) then // an error must be returned indicating that // no aggregate creation of // the CFBass COM Object can be performed. if (NULL != pUnkOuter && riid != IID_IUnknown) hr = CLASS_E_NOAGGREGATION; else { pCob = new COBass(pUnkOuter, m_pServer); if (NULL != pCob) { // We initially created the new COM object // so tell the server // to increment its global server object // count to help ensure // that the server remains loaded until // this partial creation // of a COM component is completed. m_pServer->ObjectsUp(); // We QueryInterface this new COM Object not // only to deposit the // main interface pointer to the caller's // pointer variable, but to // also automatically bump the Reference // Count on the new COM // Object after handing out this reference to it. hr = pCob->QueryInterface(riid, (PPVOID)ppv); if (FAILED(hr)) { m_pServer->ObjectsDown(); delete pCob; } } else hr = E_OUTOFMEMORY; } if (SUCCEEDED(hr)) { LOGF1("S: CFBass::CImpIClassFactory::CreateInstance Succeeded. *ppv=0x%X.",*ppv); } else { LOG("S: CFBass::CImpIClassFactory::CreateInstance Failed.");
} return hr; } STDMETHODIMP CFBass::CImpIClassFactory::LockServer( BOOL fLock) { HRESULT hr = NOERROR; LOG("S: CFBass::CImpIClassFactory::LockServer."); if (fLock) m_pServer->Lock(); else m_pServer->Unlock(); return hr; } Three parameters are passed into the method CreateInstance: ●
●
●
IUnknown* pUnkOuter: An outer IUnknown interface pointer. This pointer indicates that object aggregation is being requested by the client application. In order for aggregation to be successful, the IID passed into CreateInstance must be IID_IUnknown. REFIID riid: This variable is the interface ID (IID) that the client application is requesting when creating an instance of the COM class. PPVOID ppv: A double pointer used to store a pointer to the interface ID. PPVOID ppv is returned to the client application. In other words, the client passes in the address of the pointer to the interface ID. The server's responsibility is to create the interface pointer and return it to the calling program.
The CreateInstance method is not complicated. When this method is called, an object of class COBass is created. After the object is created, QueryInterface is called on the object using the interface ID passed to CreateInstance. If the requested interface is contained within COBass, the interface pointer is returned to the client. If the requested interface is not contained within COBass, the COBass object is deleted and NULL is returned.
Implementation of the Server Application The implementation of the COM server requires some global housekeeping. The class CServer does this housekeeping. CServer, a global variable much like an MFC CApplication object, keeps track of the number of object references within the server (see Listing 14.9).
Listing 14.9 SERVER.H--CServer Class Definition class CServer
{ public: CServer(void); ~CServer(void); void Lock(void); void Unlock(void); void ObjectsUp(void); void ObjectsDown(void); // A place to store the handle to loaded instance of this DLL module. HINSTANCE m_hDllInst; // Global DLL Server living Object count. LONG m_cObjects; // Global DLL Server Client Lock count. LONG m_cLocks; }; extern CServer* g_pServer; The server application object is passed into the constructor of all classes implemented within the COM server. The classes that accept the server application object in their constructor include the COM Object COBass, the class factory CFBass, and CImpIClassFactory. Using a central application object allows all object instances to reference global counters within the COM server. The CServer implementation is shown in Listing 14.10.
Listing 14.10 SERVER.CPP--CServer Object Implementation CServer::CServer(void) { // Zero the Object and Lock counts for this attached process. m_cObjects = 0; m_cLocks = 0; return; } CServer::~CServer(void) { return; } void CServer::Lock(void) { InterlockedIncrement((PLONG) &m_cLocks); LOGF1("S: CServer::Lock. New cLocks=%i.", m_cLocks); return; } void CServer::Unlock(void) {
InterlockedDecrement((PLONG) &m_cLocks); LOGF1("S: CServer::Unlock. New cLocks=%i.", m_cLocks); return; } void CServer::ObjectsUp(void) { InterlockedIncrement((PLONG) &m_cObjects); LOGF1("S: CServer::ObjectsUp. New cObjects=%i.", m_cObjects); return; } void CServer::ObjectsDown(void) { InterlockedDecrement((PLONG) &m_cObjects); LOGF1("S: CServer::ObjectsDown. New cObjects=%i.", m_cObjects); return; } Two variables used within the CServer object, m_cObjects and m_cLocks, are accessed by the COM class factory and COM Object. The variable m_cObjects is used to keep track of the number of interfaces requested from the COBass objects. The variable m_cLocks is used to track the number of locks issued on the COM server. When both of these variables are set to zero, the COM server is removed from memory.
NOTE: The variables m_cLocks and m_cObjects are incremented and decremented through the Win32 functions InterlockedIncrement and InterlockedDecrement. These functions are atomic operations that add and subtract 1 from the current value of the variable passed to the function. These functions are designed specifically for multithreaded applications to prevent multiple threads from modifying a value simultaneously.
Implementation of the Server Access Functions Now that the COM server internals are implemented, the server access functions must be implemented. The code for all of the server access functions is shown in Listing 14.11. Five functions must be implemented: ●
DllMain: This function must be included for all Win32 DLLs. This function is the entry point for loading libraries into memory. During loading of the DLL, a global CServer object is created. During unloading, the CServer object is deleted.
●
●
●
●
DllRegisterServer: This function is used to register the COM classes within the server in the Windows Registry. The CLSID for the COBass is registered with the CUSTOMBASS DLL as the COM server. DLLUnregisterServer: This function removes any COM classes registered by the COM server from the Windows registry. DllCanUnloadNow: This function checks the CServer object to determine whether any objects or locks remain within the server. If the respective counts are zero, TRUE is returned to the calling application. DllGetClassObject: This is the main function for accessing COM interfaces. If the CLSID for the CUSTOMBASS object is passed in, a class factory CFBASS is created. A COM interface is then requested from the CFBass class factory.
Listing 14.11 CUSTOMBASS.CPP--Server Access Function Implementation #include #include #include "..\ifish\ifish.h" #include "..\ifish\ibass.h" #include "custombassid.h" #include "comutil.h" #define _DLLEXPORT_ #include "custombass.h" #include "server.h" #include "factory.h" // We encapsulate the control of this // COM server (eg, lock and object // counting) in a server control C++ object. // Here is it's pointer. CServer* g_pServer = NULL; BOOL WINAPI DllMain( HINSTANCE hDllInst, DWORD fdwReason, LPVOID lpvReserved) { BOOL bResult = TRUE; // Dispatch this main call based on the reason it was called. switch (fdwReason) { case DLL_PROCESS_ATTACH: // The DLL is being loaded for the first time
// by a given process. // Perform per-process initialization here. // If the initialization // is successful, return TRUE; // if unsuccessful, return FALSE. bResult = FALSE; // Instantiate the CServer utility class. g_pServer = new CServer; if (NULL != g_pServer) { // Remember the DLL Instance handle. g_pServer->m_hDllInst = hDllInst; bResult = TRUE; } break; case DLL_PROCESS_DETACH: // The DLL is being unloaded by // a given process. Do any // per-process clean up here, // such as undoing what was done in // DLL_PROCESS_ATTACH. // The return value is ignored. DELETE_POINTER(g_pServer); break; case DLL_THREAD_ATTACH: // A thread is being created in a process // that has already loaded // this DLL. Perform any per-thread // initialization here. The // return value is ignored. break; case DLL_THREAD_DETACH: // A thread is exiting cleanly in a // process that has already // loaded this DLL. Perform any // per-thread clean up here. The // return value is ignored. break; default: break; } return (bResult); } STDAPI DllGetClassObject( REFCLSID rclsid, REFIID riid,
PPVOID ppv) { HRESULT hr = CLASS_E_CLASSNOTAVAILABLE; IUnknown* pCob = NULL; if (CLSID_CustomBass == rclsid) { LOG("S: DllGetClassObject: Requesting COBass."); hr = E_OUTOFMEMORY; pCob = new CFBass(NULL, g_pServer); } if (NULL != pCob) { g_pServer->ObjectsUp(); hr = pCob->QueryInterface(riid, ppv); if (FAILED(hr)) { g_pServer->ObjectsDown(); DELETE_POINTER(pCob); } } return hr; } STDAPI DllCanUnloadNow(void) { HRESULT hr; LOGF2("S: DllCanUnloadNow. cObjects=%i, cLocks=%i.", g_pServer->m_cObjects, g_pServer->m_cLocks); // We return S_OK of there are //no longer any living objects AND // there are no outstanding client locks on this server. hr = (0L==g_pServer->m_cObjects && 0L==g_pServer->m_cLocks) ? S_OK : S_FALSE; return hr; } STDAPI DllRegisterServer(void) { HRESULT hr = NOERROR; TCHAR szID[GUID_SIZE+1]; TCHAR szCLSID[GUID_SIZE+1]; TCHAR szModulePath[MAX_PATH]; // Obtain the path to this module's // executable file for later use. GetModuleFileName( g_pServer->m_hDllInst, szModulePath, sizeof(szModulePath)/sizeof(TCHAR));
// Create some base key strings. StringFromGUID2(CLSID_CustomBass, szID, GUID_SIZE); lstrcpy(szCLSID, TEXT("CLSID\\")); lstrcat(szCLSID, szID); // Create entries under CLSID. SetRegKeyValue( szCLSID, NULL, TEXT("CustomBass Class")); SetRegKeyValue( szCLSID, TEXT("InprocServer32"), szModulePath); return hr; } STDAPI DllUnregisterServer(void) { HRESULT hr = NOERROR; TCHAR szID[GUID_SIZE+1]; TCHAR szCLSID[GUID_SIZE+1]; TCHAR szTemp[GUID_SIZE+1]; //Create some base key strings. StringFromGUID2(CLSID_CustomBass, szID, GUID_SIZE); lstrcpy(szCLSID, TEXT("CLSID\\")); lstrcat(szCLSID, szID); wsprintf(szTemp, TEXT("%s\\%s"), szCLSID, TEXT("InprocServer32")); RegDeleteKey(HKEY_CLASSES_ROOT, szTemp); RegDeleteKey(HKEY_CLASSES_ROOT, szCLSID); return hr; } BOOL SetRegKeyValue( LPTSTR pszKey, LPTSTR pszSubkey, LPTSTR pszValue) { BOOL bOk = FALSE; LONG ec; HKEY hKey; TCHAR szKey[MAX_STRING_LENGTH]; lstrcpy(szKey, pszKey); if (NULL != pszSubkey) { lstrcat(szKey, TEXT("\\")); lstrcat(szKey, pszSubkey);
} ec = RegCreateKeyEx( HKEY_CLASSES_ROOT, szKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, NULL); if (NULL != pszValue && ERROR_SUCCESS == ec) { ec = RegSetValueEx( hKey, NULL, 0, REG_SZ, (BYTE *)pszValue, (lstrlen(pszValue)+1)*sizeof(TCHAR)); if (ERROR_SUCCESS == ec) bOk = TRUE; RegCloseKey(hKey); } return bOk; }
Compiling and Testing the COM Server Now that the COM server is complete, it is time to compile and test the server. Compiling the server is a trivial task. Simply select the command Build CUSTOMBASS.DLL from the Build menu. This command will build the entire project. After the server is built, it needs to be registered in the Windows Registry. You can accomplish this by selecting the command Register Control from the Tools menu. Register Control will invoke the application regsvr32.exe, which will then call the function DllRegisterServer in CUSTOMBASS.DLL. After registration is complete, the server is ready for use. In Chapter 12, a COM test application was created for testing the IFish and IBass interfaces. This application can be used to test the IFish and IBass interfaces built in CUSTOMBASS. To test CUSTOMBASS, change the CLSID referenced from CLSID_Bass to CLSID_CustomBass. When the COM test application is run, the new server will be used for accessing the IFish and IBass interfaces.
From Here... Creating your own COM classes without using a framework requires more programming than when using a framework. However, no excessive baggage from the framework is carried within the COM server, and several implementation strategies can be used during implementation. For an in-depth look at using a lightweight COM framework, refer to Chapter 13, which illustrates the development of COM Objects using the ActiveX Template Library. The ATL framework is composed of lightweight templates for designing and implementing COM Objects. Conversely, in Chapter 12, COM Objects are developed using the MFC application framework. The MFC framework is feature rich but can be overkill for COM Objects that do not need to support a user interface.
Chapter 15 Testing and Using Your Components ●
Testing and Using Your Components ❍ ActiveX Containers and Controllers ■ Using Visual Basic as a Container ■ Using Microsoft Visual C++ as a Container ■ HTML and Web Browsers ■ Listing 15.1 SAMPLEIE.HTM--Using an ActiveX Control in HTML Code ■ Listing 15.2 SAMPLENN.HTM--Converted HTML Document ■ ActiveX Control Pad ■ Using the Microsoft Access, Word, and Excel Applications as ActiveX Control Containers ❍ Tools for Testing Your Component ■ Visual C++ ActiveX Control Test Container ■ Users ■ Automated Tools ❍ From Here...
Testing and Using Your Components ●
●
●
●
●
Using Visual Basic as a container Microsoft Visual Basic provides a rapid development environment for creating containers. Check out how to use an ActiveX control in Visual Basic. Using Visual C++ as a container Microsoft Visual C++ is one of the more difficult tools to use as a container, but using the Foundation Classes makes this easier. ActiveX controls in Web pages Using ActiveX controls in HTML greatly enhances Web pages. Instead of just containing information, Web pages can now be pieces of a system. Check out how to use ActiveX controls in Web pages. Using Microsoft Access, Microsoft Excel, and Microsoft Word as containers Microsoft Office 97 has greatly improved its ActiveX capabilities. Find out how to use ActiveX controls in Microsoft Access, Microsoft Excel, and Microsoft Word. Visual C++ OLE Control Test Container Microsoft Visual C++ provides the OLE Control Test Container to test your control without ever leaving Visual C++.
Once you have created your ActiveX component, the next step is to use and test your component. You will not typically wait to try out your component until after it is completely implemented. More than likely, the first thing you will want to do is compile your code and try to use your component before you've added any significant functionality. You need to be aware of the types of container and testing applications that are available to you as a developer.
Even more critical than the actual testing of the component is testing it as soon as possible. Even with all of the banners being raised about reuse and compatibility, there are still issues to be aware of, which depend upon the tools you use. Some containers or tools may not support the features that your component supports, and even worse, in some cases, container implementations may differ, resulting in slightly different behavior. What may work in VB may result in a crash in VC++. A word of advice: Test your components often and with all of the tools that you plan to use your components with. This chapter will focus on the types and numbers of tools available that can host ActiveX Controls, as well as the tools that you can use to test your controls. Even though the primary focus of the chapter is on ActiveX Controls, almost all of the applications mentioned can launch and use ActiveX Automation Servers and even ActiveX COM objects. Because there are so many and they change so rapidly, it is beyond the scope of this book to address all of the ActiveX capabilities of all the tools available to you. You will need to research the specific tool to see to what extent it supports ActiveX and COM.
ActiveX Containers and Controllers Applications that use ActiveX controls are called container applications, or just containers. In this chapter, you will discover how to use your control in Microsoft Visual Basic, Microsoft Visual C++, Web browsers, Microsoft ActiveX Control Pad, and the Microsoft Office tools: Access, Word, and Excel. Visual Basic is probably the easiest tool to use, and Visual C++ is probably the hardest. This chapter will explain how to insert an ActiveX control into these containers, how to change the control's properties, and how to code the events contained in the control.
NOTE: If a container application is to be distributed, all controls added to a container must be distributed with the container.
Using Visual Basic as a Container To place a control in a Visual Basic application, you can open an existing project or create a new one. To create a new project, select File, New Project from the Visual Basic menu, select Standard EXE from the New Project dialog, and click the OK button. To open an existing project, select File, Open Project from the Visual Basic menu. If you want to open a recent project, select the Recent tab, select the desired project from the File list box, and click the Open button. If the project you are looking for is not listed on the Recent tab, select the Existing tab, and then select your project from the proper directory. After you have selected the project, click the Open button to open the project. Next you need to create a form or open an existing one. To create a form, select Project, Add Form from the Visual Basic menu. If the Add form dialog appears, click Form on the New tab, and then click the form in the project window. To open an existing form, click the form in the project window, and then click the View Form button. Now you need to add the control to the project. Select Project, Components from the main menu, which brings up the Components dialog (see fig 15.1). Make sure that the Selected Items Only check box, on the lower-right side of the Components dialog, is not selected. The Selected Items Only option will limit the display to the items previously selected. Search for the name of the control you want to add on the Controls tab, and click the check box next to the control. To see the path of a particular control, click the control name, and the path will display in the frame at the bottom of the dialog. FIG. 15.1 Select Project, Components to display the Components dialog.
If the control is not registered, it will not appear on the Controls tab. To select an unregistered control, click the Browse button, select your control from the proper directory, and click the Open button to register your control. After the control is checked, click the Apply button to add the control to the Toolbox, and then click the OK button to close the Components dialog. If the control file added contains more than one control, you will see more than one tool added to the Toolbox. If the MFCControl control was added, you will see three new tools, labeled with a purple OCX. If each control had a different icon, you would see three different icons on the Toolbox. If you drag the mouse over the icons on the Toolbox, tool tips appear to tell you which control you are selecting. To place the control on the form, double-click the new tool button or click the new tool button, and then click and drag the form. If the toolbox is not displayed, select View, Toolbox from the Visual Basic menu. To set the properties at design time, you need to use the Properties grid located on the right side of the screen, which lists all the changeable properties and their current values. If the Properties grid is not displayed, click the control on the form, and press F4 to display the Properties grid. The left column contains the property names, and the right contains the current property values. Click the Alphabetic tab on the Properties grid to list the properties alphabetically, or select the Categorized tab to view the properties by property category. To change a value, click the cell containing the name of the property you want to change, and then click property value cell in the same row and edit the value. Setting the properties in code is almost as easy. To set a control in code use following syntax: ControlName.PropertyName = Value ControlName is the name of the control, PropertyName is the name of the property, and Value is the new property value or a variable containing the value. Retrieving a property value is the reverse of the preceding: Variable = ControlName.PropertyNameVariable in this case is used to hold the value of the property. To execute the methods of the control, use the following syntax: Variable = ControlName.Method Parameter1,...ParameterN Variable is the name of the variable that will hold the return value of the method. If the method has no return value, Variable and the = will be eliminated. ControlName is the name of the control, Method is the name of the method, and Parameter1,...ParameterN represents any parameters to be passed to the method. The final step in the design process is coding the events. To code an event, follow these steps: 1. If the Project Explorer window is not visible, Select View, Project Explorer. The Project Explorer window appears. 2. Select the form that contains the control, and then select View, Code to display the Code window (see fig. 15.2). FIG. 15.2 Select View, Code from the Visual Basic menu to display the Code window. 3. Select the control you want to add the event code for from the Object combo box, which is the combo box on the left.
4. Select the event you want to code from the Procedure combo box, which is the combo box on the right. 5. Enter the code between the Private Sub and End Sub statements. Repeat Steps 4 and 5 to code any of the other events contained in the control. If you change your control, don't forget to recompile your Visual Basic executable to prevent errors. You can find more information on using ActiveX controls in Visual Basic in the Visual Basic help files or the Knowledge Base articles on Microsoft's Web site (http://www.microsoft.com/kb/).
Using Microsoft Visual C++ as a Container The easiest way to use your control in a Microsoft Visual C++ application is to use the Microsoft Foundation Classes (MFC) with a dialog. When you first create an application that will be using ActiveX controls, be sure to check the ActiveX Controls option in Step 3 of the MFC AppWizard (see fig. 15.3). If you do not select this option, you will have to add a call to AfxEnableControlContainer in the application's InitInstance member function (see fig. 15.4). FIG. 15.3 Here is where you check the ActiveX Controls option. FIG. 15.4 You add a call to AfxEnableControl Container in the application's InitInstance member function. To add your control to a dialog, follow these steps: 1. Select the ResourceView tab of the Project Workspace window, click the "+" next to the project's resources folder, click the "+" next to the Dialog folder, and then double-click the ID for the dialog. These steps will display the dialog in the Dialog Editor window. 2. From the Project menu, select Add To Com_ponents and Controls to display the Components and Controls Gallery dialog (see fig. 15.5). FIG. 15.5 Select Project, Add To Project, Components and Controls to display the Components and Controls Gallery dialog. 3. Double-click the Registered ActiveX Controls folder to display a list of ActiveX controls. 4. Scroll through the list of controls to find the icon for your control. Select the icon and click the Insert button to add the control to your dialog, which will bring up a message box asking "Insert this component?" Click the OK button. This will bring up the Confirm Classes dialog. 5. Click the OK button to generate the wrapper class and to add the control to the Controls Toolbar; the Confirm Classes dialog will close automatically. The wrapper class serves as an interface between your application and the control. 6. Click the Close button on the Components and Controls Gallery dialog. 7. Click and drag the control from the Controls Toolbar to the dialog to place the control on the dialog.
To set the properties of the control at design time, click the control on the dialog, and select View, Properties from the Developer Studio menu. The Control Properties dialog will appear (see fig. 15.6). Click the Keep Visible Pin to keep the dialog from closing. A property tab is available for General properties, All properties, and any property pages you defined in your control. General properties are the base set of properties assigned to all controls on the dialog. All properties are stock properties you defined in the control. To change a property value, click the tab containing the name of the property you want to change, and then set the value for the property. FIG. 15.6 Use the Control Properties dialog to set the properties for the control. The easiest way to access the properties and methods of the control in code is to create member variables that coincide with the control. To create the member variables, follow these steps: 1. Select View, ClassWizard to display the MFC ClassWizard dialog. 2. Select the Member Variables tab. 3. Make sure the Dialog class appears in the Class name text box. 4. In the Control IDs list box, select the ID for the control. 5. Click the Add Variable button to add the member variable. The Add Member Variable dialog appears. 6. In the Member variable name text box, enter a name for the member variable. A good naming standard is to enter the control name prefaced with m_. 7. Make sure the Category combo box still contains "Control," and click the OK button. The other choices are for the control's exposed properties. 8. Click the OK button on the MFC ClassWizard dialog to accept the member variable. ClassWizard defines the member variable in the dialog class and adds a Dialog Data Exchange (DDX_Control) call to the dialog's implementation of the DoDataExchange function. Now that the member variable is created, setting and retrieving properties and executing methods is fairly simple. Manipulating the properties requires using the functions in the wrapper class designed to do so. These functions are added to the wrapper class by ClassWizard. In the MFCControl control, the functions to use for setting properties include SetBackColor, SetAlignment, and SetCaptionProp. The functions for retrieving properties include GetReadyState, GetBackColor, GetAlignment, and GetCaptionProp. To use these prop- erty functions, you use the following syntax: Variable = WrapperClassVariable.Function (Parameter1,...ParameterN). Variable is used to hold a return value from the function; this will not be used if there is no return. WrapperClassVariable is the wrapper class variable ClassWizard defined in the dialog header file. Function is the name of the function being used to retrieve or set the property, and Parameter1,...ParameterN represents parameters that need to be passed to the function. Executing a method is basically the same as calling a property function. To execute a method, use the following syntax:
Variable = WrapperClassVariable.MethodName(Parameter1,...ParameterN) Variable is used to hold a return value if the method returns a value. Again, WrapperClassVariable is the wrapper class variable ClassWizard defined in the dialog header file. MethodName is the name of the method you want to execute, and Parameter1,...ParameterN represent parameters that need to be passed to the method. Okay, now for coding the events. If you have ever created event handlers for a button, radio button, or any other standard control in a dialog, you already know how to set up event handling for your custom control; the setup is the same. You use the Message Maps tab of the ClassWizard to create an event sink map, which is basically an outline, or map, of event handlers maintained by ClassWizard. When your control fires an event, the event handler that is mapped to that event is executed. To create an event handler for an event, use the following steps: 1. Select View, ClassWizard to open the MFC ClassWizard dialog. 2. Select the Message Maps tab if it is not the current tab. 3. Make sure the Dialog class appears in the Class name text box. 4. Select your control from the Object IDs list box. The Messa_ges list box now shows the events for the control (see fig. 15.7). FIG. 15.7 Messages list box shows the events for the control selected in the Object IDs list box. 5. In the Messa_ges list box, select the event you want to add code for. 6. Click the Add Function button to bring up the Add Member Function dialog. The Member function name text box contains a suggested name for the handler function. You can change the name if you like. Click the OK button when finished. If you look at the Member functions list box, you will see the new event handler function listed. The uppercase "E" in the gray box signifies that this is an event handler. 7. To add code to the handler, click the Edit Code button, which will dump you into the event handler function for that event. Add your code for the event here. More information about adding controls to Visual C++ applications can be found in the Visual C++ help files and Microsoft's Knowledge Base (http://www.microsoft.com/kb).
HTML and Web Browsers Using your control in an HTML (Hypertext Markup Language) page consists of basically three actions: placing the control in the HTML, downloading the control to the user, and installing the control on the user's machine. If all your Internet users are using the Microsoft Internet Explorer, the task of inserting an ActiveX control in an HTML document is fairly simple. If your users are using Netscape Navigator, or both, inserting a control requires a little more work. The Microsoft Internet Explorer requires the use of the HTML tag, which Netscape has not yet adopted. This tag is composed of several attributes, the most important being ID, CLASSID, CODEBASE, PARAM NAME, and VALUE. Listing 15.1 shows the HTML code that might be used with the MFCControl control.
Listing 15.1 SAMPLEIE.HTM--Using an ActiveX Control in HTML Code
Sample Page
#Version=1,0,0,1">
The ID attribute gives the control a name, providing a way for the HTML code to access the control. The CLASSID attribute, which is the unique UUID assigned to the control, tells Microsoft Internet Explorer which object to load. The UUID for your control can be found in the ODL (Object Description Library) file for your ActiveX control. Be sure to locate the UUID for the specific control you are using by looking for the class information comment for the control. For example, to locate the UUID for the MFCControlWin control used above, you would look for the following: // Class information for CMFCControlWinCtrl [ uuid(A1198546-2E75-11D0-BD82-000000000000), helpstring("MFCControlWin Control"), control ] Open the ODL file with Notepad or something similar, copy the UUID from there, and paste it in the HTML document.
NOTE: Even if you have only one control in your OCX file, this file contains many UUIDs. Be sure to locate the proper UUID using the method above. Also, your UUID will not be the same as the one in the example.
If the current version is not on the user's machine, the CODEBASE attribute tells Microsoft Internet Explorer where to find the control to download. Once the current version is loaded on a user's machine, the HTML document uses the control from the user's machine, allowing the document to load faster. Keep in mind that if your control was built with the Microsoft Foundation Classes, the MFC DLLs have to be loaded on the user's machine. This adds to the size and complexity of the download. The bigger the download, the longer the user has to wait to load the control. If your control is built using the BaseCtl, the CODEBASE statement refers to the control only. If you created your control with MFC, you need to use a new technology from Microsoft called Cabinet files (CAB files for short). These files allow you to compress a group of files into one file (the CAB file), download the file to the user's PC, and install the files on the user's PC. The CAB file contains an INF file, which controls the installation. Check out Microsoft's Web site (http://www.microsoft.com/) and the ActiveX SDK documentation for more information on CAB files.
NOTE: Even if you created your control without the Microsoft Foundation Classes, you can still use a CAB file to compress your control and save transfer time.
The tag is used inside the tag to set the initial property values of the control. The tag has two attributes: NAME and VALUE. The NAME is the name of the property, and the VALUE is the property value. Netscape Navigator users will need to purchase a plug-in from NCompass Labs called ScriptActive. This plug-in allows Navigator to run HTML documents that contain ActiveX controls. The information to be used by this add-in is contained in an tag because Navigator does not recognize the tag. If your Web page will be read using Netscape Navigator and Microsoft Internet Explorer, which is most likely the case, nest the tag within the tag. Microsoft Internet Explorer will fail if the tag is outside the tag. ScriptActive comes with the HTML Conversion Utility, which creates a Netscape HTML file from a Microsoft Internet Explorer HTML file, making the developer's life easier. Create your page with ActiveX Control Pad and use the utility to automatically create a page compatible to both browsers. The HTML document (see Listing 15.2) is an example of a document converted by the HTML Conversion Utility. Visit the NCompass Labs Web site (http://www.ncompasslabs.com/ )for more information.
Listing 15.2 SAMPLENN.HTM--Converted HTML Document
Sample Page
Another powerful attribute is SCRIPT. This attribute allows a developer to add code directly to an HTML document. Currently, two scripting languages are available: JavaScript and Visual Basic Script (often referred to as VBScript),
which imitate Java and Visual Basic, respectively. Check out Netscape's Web site (http://home.netscape.com/) for JavaScript syntax and information and Microsoft's Web site (http://www.microsoft.com/) for VBScript syntax and information. Chapter 16 explains scripting in a little more detail.
NOTE: Unless you have certified your control, make sure that the Safety Level is not set to High in Microsoft Internet Explorer. If it is set to High and your control is not certified, it will not run your scripts. To check or change this setting, select View, Options from the Explorer menu. Select the Security tab, and then click the Safety Level button to display the Safety Level dialog. Most users, especially developers, choose Medium because it gives you the option to load or not load something. Certification and other security issues are covered in Chapter 16.
ActiveX Control Pad The number of Web Authoring tools is growing rapidly; very little manual coding of HTML is done anymore. The ActiveX SDK includes a handy little tool called the ActiveX Control Pad, which aids in managing ActiveX controls in Web pages. It allows you to easily add controls and create VBScript and JavaScript scripts. It also integrates a WSIWYG design area for authoring 2-D layouts in conjunction with Microsoft's HTML Layout Control. When you first bring up Control Pad, it creates an initial blank HTML document and opens the document in the Text Editor (see fig. 15.8). FIG. 15.8 When you first bring up Control Pad, it creates a blank HTML document. To insert an ActiveX Control, select Edit, Insert ActiveX Control, which opens the Insert ActiveX Control dialog. This dialog displays a list of all registered ActiveX controls. Select the control you want to insert from the Control Type list box on the Insert ActiveX Control dialog, and click the OK button. A form displaying the control appears along with a Properties grid. Use the mouse to size the control.
NOTE: Version 1.0 of the ActiveX Control Pad has some bugs in its refreshing functionality. Once in a while, items appear to smear when you are sizing or positioning them, but they aren't. Once you close the editor in use and open it again, things are back to normal.
Set the initial values of the properties using the Properties grid. Select the property you want to change, and then enter the new value at the top of the Properties grid. After you have entered the new value, click the Apply button to save the setting. When you exit the form, the necessary HTML is generated and added at the current cursor position. Figure 15.9 is an example of HTML code generated with the ActiveX Control Pad. FIG. 15.9 HTML is generated for the added control. This method of inserting an ActiveX control works fine if you don't care where the control or controls are placed, but if you want the control(s) at specific X and Y coordinates, you need to use an HTML layout because HTML currently cannot recognize X, Y, and Z order positioning. HTML layouts allow exact positioning of controls on a Web page. The ActiveX Control Pad saves the HTML layout in a file with an ALX extension. When a browser reads the HTML that contains an HTML layout, it loads the HTML layout from the ALX file. Control Pad provides the HTML Layout
Control for creating and editing these layouts. The HTML Layout Control gives the developer a forms-based Web page development environment similar to the Visual Basic interface. To create an HTML layout, select File, New HTML Layout to open the HTML Layout Editor. You can click and drag any of the default controls on the Standard or Additional tabs onto the form. To set the properties, select View, Properties to display the Properties grid. To add your ActiveX control, click the right mouse on the bottom of the tab in the Toolbox where you want the control to reside, and select Additional Controls to bring up the Additional Controls dialog (see fig. 15.10). FIG. 15.10 The additional Controls dialog lists all registered ActiveX controls. Select the control you want to add by clicking the check box next to its name in the Available Controls list box. Click the OK button to add the control to the tab, and close the Custom Controls dialog. Now you can use that control as if it were part of Control Pad. After you have your controls set up, close the form and save the changes. This course returns you to the HTML Source Editor. Now that you have created the layout, you need to add it to the HTML. Click an insertion point anywhere inside the tags, and then select Edit, Insert HTML Layout. Select the layout you want to insert, and then click the OK button. The needed HTML will be generated and placed at the cursor. The Control Pad is also helpful for creating scripts. Before creating a script, make sure you set the Script Language for the page. Select Tools, Options, Script to bring up the Script Options dialog. Select the scripting language you prefer, and then click the OK key to save your selection. To create the script, open the Script Wizard by selecting Tools, Script Wizard. Scripts can be created using the List View or the Code View. List View allows you to insert actions while Code View is more of a code editor. To create a line of code, select an event from the Select an Event list box. This selection determines which event the script is being created for. After the event is selected, select an action, and double-click it to insert it into the script. If you are in List View, you will be prompted to enter any needed parameters for the selected action. If you are in Code View, a skeleton of the needed command will appear in the Script Pane; you will need to edit the necessary pieces. You can also add code in the Script Pane. When you are finished creating the necessary scripts, click the OK key to add the scripts to the HTML code. For more information on Control Pad, refer to Control Pad's help files and the Author/Editing section of Microsoft's Web site.
Using the Microsoft Access, Word, and Excel Applications as ActiveX Control Containers When you design a control, you probably think, "It has to work in Microsoft Visual C++, Microsoft Visual Basic, and on the Internet." What about Office products? For example, what if someone wants to use a fancy list box control you've created in a Microsoft Word Document to list the sections of a long document, allowing the users to pick the section they want to jump to. Microsoft Office 97 makes it easy to use an ActiveX control in an Office application, even in Microsoft Word. ActiveX controls can be a very useful addition to Microsoft Office 97, especially in Microsoft Access, Microsoft Word, and Microsoft Excel. So how do you add controls to these products? The Microsoft Access menu choices are a little different than Microsoft Word and Microsoft Excel, but the concepts are the same. Basically, you choose More Controls from the Controls Toolbox, select your control from the list, set the properties, and add code to the events.
To add a control to a Microsoft Access form, follow these steps: 1. To open or create a new form, click the Forms tab of the Database window, which appears when you first open an existing database or create a new one. To create a new form, click the New button. This action opens the New Form dialog. Select Design View from the list box, and click the OK button. To select an existing form, select the form from the list of forms on the forms tab of the Database window, and then click the Design button on the Forms tab. This action opens the form in Design View. 2. If the Toolbox is not visible, select View, Toolbox from the Access menu to display it (see fig. 15.11). FIG. 15.11 Note the More Controls icon at the bottom of the Toolbox. 3. Select the More Controls icon from the Toolbox. This displays a list of registered controls. 4. If your control does not appear, it probably was not registered. To register it, select Tools, ActiveX Controls from the Microsoft Access main menu, which displays the ActiveX Controls dialog. Click the Register button, and select your control's path from the Add ActiveX Control dialog. Then click the Open button to register it. 5. Select the control you want to add, and then click on the form where you want the control to appear. To add a control to a Microsoft Word document or a Microsoft Excel spreadsheet, follow these steps: 1. Open an existing document or spreadsheet, or use the one opened when you enter. To create a new document or spreadsheet, click File, New to open the New dialog. Click the OK button to use the defaults. 2. If the Control Toolbox is not visible, select View, Toolbars, Control Toolbox from the main menu to display it. 3. Select the More Controls icon from the Control Toolbox. This displays a list of registered controls (see fig. 15.12). FIG. 15.12 Select the More Controls icon to display a list of registered controls. 4. If you need to register your control, select the last item in the list, Register Custom Control, to display the Register Custom Control dialog. Select your control's path from the Register Custom Control dialog, and then click the Open button to register the control. 5. Select the control you want to add. Microsoft Word places the control at the cursor position. To place the control on a Microsoft Excel spreadsheet, click on the spreadsheet where you want the control to appear. Next you need to set the properties for the control. Make sure you are in Design Mode. Design Mode can be toggled using the View icon on the Form View toolbar in Microsoft Access (see fig. 15.13) and the Design Mode icon on the Control Toolbox in Microsoft Word and Microsoft Excel (see fig. 15.14). To set the control's properties, click with the right mouse on the control, and select Properties from the pop-up menu to display the Properties grid. Edit the properties as needed. You can click with the right mouse on the control and select xxxx Control Object, Properties to use the property pages to edit the properties; xxxx represents the control name. FIG. 15.13 Toggle Design Mode in Microsoft Access using the View icon on the Form View Toolbar.
FIG. 15.14 Toggle Design Mode in Microsoft Word and Microsoft Excel using the Design Mode icon on the Control Toolbox. To create code for events, make sure you are in Design mode. Right-click the control, and select Build Event in Microsoft Access, or View Code in Microsoft Word and Microsoft Excel to open the code window. The code window for Microsoft Excel is shown in Figure 15.15. FIG. 15.15 The code window for Microsoft Word looks the same as the code window for Microsoft Excel. You can now code as you would in a Visual Basic code window. To debug your code, press the F5 key or use the Debug menu. You can also add an ActiveX control to a report the same way you add it to a form. For more detailed information, refer to the Microsoft Office 97 Help files and Microsoft's Knowledge Base found on its Web site (http://www.microsoft.com/kb/). If you compile the project, be sure to recompile if you change the code for your ActiveX control.
Tools for Testing Your Component To ensure that your ActiveX control works correctly once in the user's hands, it needs to be thoroughly tested. Thorough testing involves completely testing every event, method, and property. The control must also be tested on every platform and, to the extent possible, with as many of the container applications used by your users as possible. This part of the chapter discusses using the OLE Control Test Container packaged with Microsoft Visual C++, your users, and automated testing tools to test your ActiveX control. You can also use the tools mentioned earlier in the chapter.
Visual C++ ActiveX Control Test Container The ActiveX Control Test Container can be used to test the properties, methods, and events functionality of ActiveX controls. You can test the persistence of controls by saving properties to a stream or substorage, reloading properties, and viewing the stored stream data. The ActiveX Control Test Container can be integrated with the Visual C++ debugger, allowing you to step through the control's code. After you compile and link your control, you can use the Test Container to change properties, invoke methods, and fire events to test the control. Select Tools, ActiveX Control Test Container from the Developer Studio menu to load the Test Container. The first step is to insert your control into the ActiveX Control Test Container. Select Edit, Insert OLE Control. This displays the Insert OLE Control window (see fig. 15.16). Select your control from the Object Type list box, and then click the OK button to insert your control. FIG. 15.16 The Insert OLE Control window displays all registered ActiveX controls in the Object Type list box. If your control is not in the list box, it probably was not registered. Click the Cancel button to close the Insert OLE Control window, and then use the following steps to register your control:
1. Select File, Re_gister Controls to open the Controls Registry window. 2. Click the Register button. Use the Register Controls window to locate your control. 3. Select your control, and click the Open button to register the control. 4. Click the Close button to close the Controls Registry window and return to the main window. Now that the control is inserted, you can test the property, event, and method functionality of your control. You can test changing a property through its property sheets or property dialog. To change a property via its property sheet, use the following steps: 1. Click the control you inserted in the main window of Test Container to select the control. 2. Select Edit, Properties... xxx Control Object, where xxx is the name of the control. This step displays the control's property page. 3. Edit the value of the property. 4. Click the Apply button to set the property to the new value. 5. Click the OK button to close the control's property page. To change a property via its property dialog follow these steps: 1. Click the control you inserted in the main window of Test Container to select the control. 2. Select View, Properties to show the Properties dialog for the control (see fig. 15.17). FIG. 15.17 All of the control's properties may be changed using the Properties dialog. 3. Select a property from the Property combo box. Edit the value in the Value text box. 4. Click the Apply button to set the property to the value. If you enter a value that is the wrong data type, you will hear a beep, and the value will not be changed. 5. Click the Close button to close the Properties dialog. If you need to see when a property value changes, select View, Notification Log to open the Notification Log window. Whenever a property changes, a message will appear in this window (see fig. 15.18). If the property you are changing supports the OnRequestEdit notification, use the OnRequestEdit( ) Response radio buttons on the Notification Log dialog to see how your control reacts to the different responses from OnRequestEdit. FIG. 15.18 Use the Test Container's Notification Log dialog to show the data-binding notifications when a property value changes. To invoke a method, follow these steps: 1. Click the control you inserted in the main window of Test Container to select the control.
2. Select Edit, Invoke Methods to open the Invoke Control Method dialog. 3. Select a method from the Name combo box at the top of the Invoke Control Method dialog (see fig. 15.19). FIG. 15.19 Select a method from the top section of the Invoke Control Method dialog, set the para-meter values in the middle section, and view the method's return value in the bottom section. 4. Enter the needed values in the parameter text boxes shown in the center of the Invoke Control Method dialog. 5. Click the Invoke button to invoke the method. If there is a return value from the method, it will display at the bottom of the Invoke Control Method dialog. 6. When you finish invoking methods, click the Close button to close the dialog. To fire an event, perform an action that will cause the event to fire. For example, if you are testing the MFCControlWin control, you could invoke the CaptionMethod method to fire the Change event. To view which events are firing, select View, Event Log to display the Event Log dialog. When an event fires, the call to the event handler is displayed in the Event Log dialog (see fig. 15.20). FIG. 15.20 When an event fires, the call to the event handler is displayed in the Event Log dialog. To select which events are displayed, open the Events dialog for the control by selecting Edit, View Event List. An example of the dialog is shown in Figure 15.21. To toggle between showing and not showing an event, select the event and click the Log/No Log button. To log all events, click the Log All button. To not log any events, click the Log None button. Click the Close button when you are finished. FIG. 15.21 If you want to display the events for the MFCControl, this is how the Events dialog will look. The ActiveX Control Test Container provides a way for you to test the functions in your control. Select Edit, Embedded Object Functions to see a submenu of the functions. Select a function to execute, and that function will be executed. Table 15.1 lists the functions with a brief description.
Table 15.1 Embedded Object Functions Action
Description
Primary Verb
Invokes control's primary verb.
Activate
Activates control and puts it in Loaded state.
UI Activate
Puts control in UI Active state.
Close
Closes control and puts it in Loaded state.
Deactivate
Deactivates control and puts it in the Loaded state.
Deactivate UI Only
Restores OLE Control Test Container's original state.
Hide
Hides control and puts it in Loaded state.
Open
Puts control in stand-alone mode and Open state.
Reactivate and Undo
Reactivates control and puts it in Loaded state.
Run
Runs control and puts it in Loaded state.
Show
Activates control and puts it in UI Active state.
Properties
Shows property sheet for control.
The Test Container can run in two modes: Passive Container Mode or Simulated Design Mode. When Test Container is in Passive Container Mode, it does not automatically change the control's state. When it is in Simulated Design Mode, it does change the control's state automatically to mimic Design mode. A powerful feature of the Visual C++ Debugger is the fact that you can integrate an executable. This means you can use OLE Control Test Container, or another container, to test your control and be able to step through the code. You could use the Visual Basic executable to test design mode and use a Visual Basic executable to test run mode. To set this up, use the following steps: 1. Select Project, Settings from the Developer Studio menu to open the Project Settings dialog. 2. If you have not compiled and linked in Debug mode recently, you should compile and link the debug version before continuing. The debug version must match the registered version for the debugger to work correctly. 3. Make sure that Win32 Debug is selected in the Settings For combo box. 4. Select the Debug tab (see fig. 15.22). FIG. 15.22 Make sure Win32 Debug is selected in the Settings For list box, and then select the Debug tab. 5. Enter the name of the executable you want to use with the debugger in the Executable for debug session text box. If you want to use the OLE Control Test Container, enter TSTCON32.EXE with the proper path. It can be found in the BIN directory of your Visual C++ directory. If you want to use the Visual Basic development environment, enter VB5.EXE with the proper path. You will find VB5.EXE in your Visual Basic directory. 6. Click the OK button to save the settings, and exit the Project Settings dialog. 7. Set any breakpoints you need to debug your control. 8. Select Build, Start Debug, Go, or press F5 to start the debugger. 9. A Microsoft Developer Studio dialog will appear, alerting you that there is no debug information for TSTCON32.EXE, or VB5.EXE if you're using Microsoft Visual Basic. It also asks you to press the OK button to continue. Click the OK button; you don't need to debug information for TSTCON.32EXE or VB5.EXE. If you want to prevent this dialog from appearing again, click the Do not _prompt in the future check box. 10. Use the container as needed. The debugger will pause at the breakpoints after switching from the container application to the debugger. You can step through code, check values, and so on. 11. When finished, exit the control application, or select Debug, Stop Debugging to exit the debugger.
Users One of the best testing tools is your user community. If you correctly analyze the needs of your users before you even
begin to create a control and use that knowledge during development, you will have a more marketable and widely used control. You also need to involve your user community in the testing of the control. Their feedback is needed not only to report bugs, but also to let you know how they like the look and feel. Feedback on the look and feel lets you know if you missed anything during the needs analysis and if your control will be popular. When choosing users to be beta testers, include users with many different needs. For example, don't have all your beta testers use your control with only Visual C++. Select some who will be using your control with Visual C++, Visual Basic, the Internet, and so on. Make sure your group of beta testers will fully test the control's functionality. If you are developing controls for internal use, make sure the end users you select will use the system you are creating in a manner that fully tests your control. Make sure that you stay in constant communication with your beta testers either through personal contact or through support personnel. Keeping in touch will let you know how they like the product. It will also let you know that the control is being well tested and that you are resolving all bugs. Make sure you fully explain the functionality of your control to the users. They can't test well if they don't understand what they are testing. When users report a bug, ask them to explain exactly what they did to get the bug and what they did just before the bug occurred. This process may take a couple of phone calls or e-mails. The bug could be caused by something they did earlier; get as much information as possible. Next try to duplicate the problem. When duplicating the problem, try to create their environment--exactly. For example, if they are using Windows 95, try to duplicate the problem on Windows 95, not Windows NT. This applies to hardware as well. If they are using a machine that is slow and doesn't have much memory, don't try to re-create the problem on a souped-up development machine. We know this is difficult to do, but the closer you are to their environment, the quicker you will find the problem, and the better your chance of solving the real problem. When you or your users find a bug, it is a good idea to try the control in different types of test containers to see how it behaves. Because different containers use ActiveX components in different ways, the problem may be in the way a container is using the control. For example, Microsoft Visual Basic uses an ActiveX control differently than the Visual C++ OLE Control Test Container. How a control behaves in different types of containers may also give you a better idea of what the problem is. The Microsoft OLE/COM Object Viewer tool (OLEView.exe) included with the Microsoft ActiveX SDK is also a good tool for debugging. It allows you to view the interfaces and components of an ActiveX control. Microsoft OLE/COM Object Viewer shows you what interfaces, constants, properties, and methods are contained in the registered copy of the ActiveX control. If the components of the control don't appear as you expect, maybe your ActiveX object didn't register correctly. Use The Microsoft OLE/COM Object Viewer to display the registry entries for the control. The Microsoft OLE/COM Object Viewer is a good tool for checking the setup of your ActiveX objects. A copy of the Microsoft OLE/COM Object Viewer is available in the \Bin directory of the Microsoft ActiveX SDK directory or from Microsoft's Web page on the OLE/COM Object Viewer tool (http://www.microsoft.com/oledev/olecom/oleview.htm). Usually the Web site contains the most recent copy of the OLE/COM Object Viewer.
Automated Tools Automated testing tools can also be used to test your control. These tools allow you to build scripts that store keystrokes to create unattended, consistent automated tests. The same script can be run across multiple machines, ensuring the same tests are run on different platforms. This helps to eliminate problems occurring on one platform and not another. If you add a new feature, you can add the needed tests to the script once and copy the script across all test machines. You don't have to manually retest on every platform, saving time and money. One testing tool that supports testing ActiveX controls is the Rational Visual Test. (Rational recently acquired this product from Microsoft.) Rational plans on integrating Visual Test with its Rational Rose visual modeling tool. You
can find information on testing ActiveX controls in the Microsoft Knowledge Base and in the Visual Test help files.
From Here... This chapter has focused on using and testing your ActiveX component implementation. While the majority of the discussions revolved around ActiveX Controls, it is important to note that the same techniques can be applied to just about any component you develop. In addition to the tools we pointed out, more are appearing every day that incorporate ActiveX, including FrontPage and Visual InterDev and Microsoft BackOffice applications, such as Microsoft Transaction server and Microsoft SQL Server. When creating, using, and distributing your component, you need to be as thorough as possible. Give the component to as many users as is feasible and test in as many containers as is practical.
Chapter 16 Advanced Topics ●
Advanced Topics ❍ Internet ■ Internet Security ■ Signing Software ■ Internet Scripting ■ Listing 16.1 JSVBSSAMP.HTM--Example of VBScript and JavaScript Using the
Listing 16.2 AREFSAMP.HTM--Example of JavaScript Using the Tag Click here to view the message
Internet Component Download An important piece of the ActiveX Internet technology is the capability to safely download and install ActiveX controls and the needed support files on the client machine. Microsoft Internet Explorer automatically downloads and installs ActiveX controls used in HTML documents through a process called Internet Component Download. A control is downloaded only if the control is not installed on the users' machines or if the version used in the HTML is newer than the control on the users' machines. For now, the control remains on the users' machines until they remove it. Microsoft has plans to provide a mechanism, in future releases of Internet Explorer, to delete unused controls from a user's machine. Before an ActiveX control is installed, Internet Explorer checks for a digital signature. As mentioned earlier in this chapter, Internet Explorer has three safety levels: high, medium, and low. High will not install an unsigned control, medium asks users if they want to download an unsigned control, and low downloads a control signed or unsigned. Digital signatures were covered earlier in this chapter. Once the control is downloaded and installed, an attempt is made to register the control and its components. For a control in an HTML page to automatically download, you need to use the CODEBASE attribute of the tag. An example of this is shown in the HTML code listed in Listing 16.3.
Listing 16.3 SAMPLEIE.HTM--Using an ActiveX Control in HTML Code
Sample Page
The CODEBASE attribute tells Microsoft Internet Explorer what to download and install. The CODEBASE attribute contains a reference to where the control and its supporting files can be found for downloading. If the control needs supporting files, the CODEBASE attribute points to a cabinet (CAB) file or an install (INF) file. These files, like an ActiveX control, can contain a digital signature. A CAB file is a file that contains a compressed version of the control and any other files the control needs to install and run. It is downloaded and expanded, and the control's components are installed. To create a CAB file, use the Diamond utility provided with the Microsoft ActiveX SDK. An INF file specifies the files that need to be downloaded and their URLs. Each file is downloaded and then installed. The INF file can provide platform independence by specifying different URLs for files that need to be downloaded for different platforms. The CODEBASE attribute should contain a version number to allow Microsoft Internet Explorer to check whether the version of the file on the Web server is newer than the same file installed on the user's machine. To include a version number, use the Version URL fragment as shown in Listing 16.3. The numbers after the = represent the current version of the control, which can be found by looking at the properties of the control. If a version number is not used, Microsoft Internet Explorer will assume that the version of the file on the user's machine is recent enough. For more information on the Internet Component Download, see the Internet Component Download section of the Microsoft ActiveX SDK documentation.
Electronic Commerce Electronic commerce could be defined as doing business by using a computer. For your purposes, it is more specifically defined as doing business over the Internet. It could be shopping via one of the Internet malls, buying and selling personal computers via a reseller's Web site, online banking, or just about anything. Some of the many advantages of doing business on the Internet are that companies are able to reach people 24 hours a day, seven days a week; it's easier to reach global customers; the number of Internet users is growing rapidly; information reaches people faster through the Internet than through conventional methods; and new products can be released more quickly. One of the big disadvantages has been security. Server security has been around for a while, but a
standard technology for the secure transfer of sensitive data was missing until recently. The Secure Channel communication technology, which provides privacy, integrity, and authentication for the transfer of data from client to server and server to server, helps to solve this problem. As mentioned earlier in this chapter, SSL handles the authentication of the server, encryption of the data sent and received, and integrity of the data being sent and received. PCT provides protection against eavesdropping on a network or altering a network packet. Before this technology was developed, credit card information and other sensitive information could not be entered online. If people wanted to buy something they saw on a merchant's Web site, they had to call the merchant to place an order. However, the current technology makes ordering possible from the Web site; shopping and ordering are done in one place. Examples of this are Dell Computer (http://www.dell.com) and Gateway2000 (http://www.gateway2000.com). Both Web sites allow users to configure their own system, do what-if price analysis, and place the order including payment information. Another protocol, which is in the final stages of development, will further secure electronic transactions. This protocol, Secure Electronic Transactions (SET), is designed to handle secure credit card payments over the Internet using digital certificates and cryptography. The Netscape Merchant System and the Microsoft Merchant Server are specialized systems for developing Web merchandising sites. These systems provide many built-in features to help companies create a complete shopping Web site. The features include, but are not limited to, billing, transaction processing, product updating, product searching, storefront creation, handling of secured payments, order processing, and database access. These systems are marketed as a total merchandising Web server solution. The use of the Internet for commercial and recreational purposes expands every day. The need for reliable security is unprecedented. Fortunately, a lot of people and companies are working hard to make the Internet a reality for all types of use. The next sections examine some of the advanced features of ActiveX.
Advanced COM In this section, we dig into the details of the Component Object Model (COM). In the first two subsections, we deal with COM fundamentals. First we examine how C++ vtables are used to implement COM interfaces. Second we show how an ActiveX Object can aggregate another ActiveX Object to implement part of its functionality, and we discuss ATL tools for aggregation and tear-off interfaces. Finally we look at enumerators.
Using C++ vtables to Describe Interfaces Roughly, a COM interface is a structure that contains a pointer to a structure containing pointers to functions. Rather than read that over, have a look at the C definition of IUnknown in Listing 16.4.
Listing 16.4 MSDEV\INCLUDE\UNKNWN.H--The Definition of the IUnknown Interface in C Is a Structure that Contains a Pointer to a Structure that Contains Pointers to Functions typedef struct IUnknownVtbl { BEGIN_INTERFACE HRESULT ( STDMETHODCALLTYPE __RPC_FAR *QueryInterface )( IUnknown __RPC_FAR * This, /* [in] */ REFIID riid, /* [out] */ void __RPC_FAR *__RPC_FAR *ppvObject); ULONG ( STDMETHODCALLTYPE __RPC_FAR *AddRef )( IUnknown __RPC_FAR * This); ULONG ( STDMETHODCALLTYPE __RPC_FAR *Release )( IUnknown __RPC_FAR * This); END_INTERFACE } IUnknownVtbl; interface IUnknown { CONST_VTBL struct IUnknownVtbl __RPC_FAR *lpVtbl; }; IUnknownVtbl is the structure that contains pointers to functions. It's identical to the table of virtual functions, or vtable, that C++ establishes for a class's virtual functions. IUnknown is a structure that contains a pointer to an IUnknownVtbl structure. So to simplify the definition, an interface is a structure that contains a pointer to a vtable. Because COM defines an interface this way, the definition of an interface in C++ is simpler than in C. Look at the C++ definition of IUnknown in Listing 16.5
Listing 16.5 MSDEV\INCLUDE\UNKNWN.H--The Definition of an Interface in C++ Makes Use of C++ vtables interface IUnknown { public: BEGIN_INTERFACE virtual HRESULT STDMETHODCALLTYPE QueryInterface( /* [in] */ REFIID riid, /* [out] */ void __RPC_FAR *__RPC_FAR *ppvObject) = 0; virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0; virtual ULONG STDMETHODCALLTYPE Release( void) = 0; END_INTERFACE };
In C++, the interface is simply a structure (a class, really) that has virtual functions, and the vtable is implicit in the language. With that in mind, now look at various ways an ActiveX Object can implement multiple interfaces. CNumber: A Simple Sample The first sample is a COM Object that implements a simple custom interface. The custom interface is INumber, shown in Listing 16.6. INumber defines a method to get the value of the number and a method to set the value of the number. CNumber is a COM Object that implements the INumber interface. The CNumber class is shown in Listing 16.7.
Listing 16.6 INUMBER.H--The INumber Interface interface INumber : public IUnknown { public: // ILrsInetUnlock methods virtual HRESULT __stdcall GetNumber( /* [out] */ double* pValue) = 0; virtual HRESULT __stdcall SetNumber( /* [in] */ double value) = 0; };
Listing 16.7 UMBER.H--The CNumber Class class CNumber : INumber { public: // IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); // ILrsInetUnlock methods HRESULT __stdcall GetNumber(double* pValue); HRESULT __stdcall SetNumber(double value); // Constructors and destructor CNumber(); //CNumber(LPUNKNOWN pUnkOuter); ~CNumber(); private: ULONG m_cRef; double m_value; }; CNumber's vtable precisely matches the definition of the INumber interface. Because INumber is
derived from IUnknown (you should never encounter an interface that isn't), the vtable for CNumber can be used as the IUnknown interface as well. In Listing 16.8, you can see that QueryInterface returns this whether the INumber or IUnknown interface is requested (when riid is IID_IUnknown).
Listing 16.8 UMBER.CPP--CNumber::QueryInterface HRESULT CNumber::QueryInterface(REFIID riid, LPVOID* ppvInterface) { if(IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_INumber)) { *ppvInterface = this; AddRef(); return NOERROR; } else { return E_NOINTERFACE; } } In some cases, you'll need more than one vtable. For example, if you need to implement multiple interfaces, such as INumber and IPersistStorage, you simply can't define a vtable that satisfies both interfaces. Or if you're going to make your object aggregatable, you need to have a separate vtable for the true IUnknown interface. This is discussed in more detail in the section about Aggregation later in this chapter. CNumber1: Separate vTables The CNumber1 class is another COM Object that implements the INumber interface (see Listing 16.9). The CNumber1 class complicates things a little by implementing the INumber interface using the vtable of an embedded class.
Listing 16.9 UMBER1.H--The CNumber1 Class: Multiple vtables class CNumber1 : IUnknown { public: // IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); class ImpINumber : INumber {
// IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); // ILrsInetUnlock methods HRESULT __stdcall GetNumber(double* pValue); HRESULT __stdcall SetNumber(double value); // A macro to gain access to the CNumber1 "this" // pointer from within the embedded class #define GET_CNUMBER1(pThis) \ CNumber1* pThis = \ ((CNumber1*)((BYTE*)this - \ offsetof(CNumber1, m_impINumber))); } m_impINumber; friend class ImpINumber; // Constructors and destructor CNumber1(); ~CNumber1(); private: ULONG m_cRef; double m_value; }; Here the ImpINumber class is defined within the CNumber1 class, and the instance m_impINumber is declared as a member of the CNumber1 class. The vtable of CNumber1 matches the IUnknown interface, and the vtable of ImpINumber matches the INumber interface. CNumber1's QueryInterface returns this when IUnknown is requested and the address of m_impINumber when INumber is requested. Listing 16.10 shows the implementation of CNumber1::QueryInterface.
Listing 16.10 UMBER1.CPP--CNumber1::QueryInterface HRESULT CNumber1::QueryInterface(REFIID riid, LPVOID* ppvInterface) { if(IsEqualIID(riid, IID_IUnknown)) { *ppvInterface = this; AddRef(); return NOERROR; } else if(IsEqualIID(riid, IID_INumber)) { *ppvInterface = &m_impINumber;
AddRef(); return NOERROR; } else { return E_NOINTERFACE; } } One complicating factor is that the class ImpINumber doesn't have immediate access to the members of CNumber1. In Listing 16.9, you see that ImpINumber is declared as a friend of CNumber1. The friend declaration gives ImpINumber access to CNumber1's members, but ImpINumber still doesn't have a pointer to CNumber1's members. Within the ImpINumber class, you declared the macro GET_CNUMBER1 that calculates the address of CNumber1 based on the address of the embedded class ImpINumber. Using GET_CNUMBER1, you can get a pointer to the CNumber1 object from within the methods of the ImpINumber class. This macro applies the technique used by MFC's METHOD_PROLOGUE set of macros.
The GET_CNUMBER1 Macro The GET_CNUMBER1 macro is used by the ImpINumber class, which is embedded in the CNumber1 class, to gain access to the CNumber1 class's members. GET_CNUMBER1 subtracts the offset of the ImpINumber class within the CNumber1 class from the address of the ImpINumber object (this) to determine the address of the CNumber1 object.
Another complicating factor of this implementation is that INumber is derived from IUnknown, so INumber includes QueryInterface, AddRef, and Release methods. The embedded ImpINumber class must implement these methods. It does so by calling CNumber1's corresponding methods. Listing 16.11 shows ImpINumber's implementation of QueryInterface.
Listing 16.11 UMBER1.CPP-CNumber1::ImpINumber::QueryInterface HRESULT CNumber1::ImpINumber::QueryInterface(REFIID riid, LPVOID* ppvInterface) { GET_CNUMBER1(pThis); return pThis->QueryInterface(riid, ppvInterface); } Now that you've established how C++ vtables can be used to implement interfaces in different ways, take a look at how vtables are used in aggregation.
Reusing ActiveX Objects with Aggregation COM Objects don't use inheritance to reuse the implementations of existing objects. Aggregation is used instead of inheritance. In aggregation, one object creates another object and reuses its interface implementations. Where traditional inheritance has a base class and a subclass, aggregation has an aggregated object and outer object. The outer and aggregated objects are presented to the rest of the system as if they were a single object. In this sample, you create CNumber2, which is an aggregatable implementation of INumber. Then you create CNumber3. CNumber3 will implement the IWholeNumber interface and aggregate a CNumber2 object. The aggregated CNumber2 object will provide the implementation of the INumber interface for the CNumber3 object (see fig. 16.1). FIG. 16.1 CNumber3 aggregates CNumber2 to reuse its implementation of the INumber interface. CNumber2: An Aggregatable Object An object is aggregatable if it follows some rules in its implementations of the IUnknown interface. ●
●
●
The aggregatable object must handle the punkOuter argument of the class factory's CreateInstance function. The punkOuter argument is the pointer to the IUnknown interface of the outer object. The aggregatable object must delegate QueryInterface calls to the outer object. The exception to the preceding rule is that the QueryInterface method of the aggregatable object's true IUnknown interface does not delegate to the outer object.
The True IUnknown Interface Every COM Object has an IUnknown interface implementation. When you use aggregation, you create a complex COM Object. This complex object presents only one IUnknown interface to the rest of the system. The aggregated object's IUnknown is hidden, known only to the outer object. The hidden IUnknown of the aggregated object is known as its true IUnknown interface.
NOTE: In a typical implementation of an aggregatable object, the AddRef and Release methods of interfaces other than the true IUNKNOWN are delegated to the outer object, but alternative reference counting schemes are possible.
In order to meet these requirements, the aggregatable object must have a separate vtable for its true
IUnknown interface. CNumber1 already has separate vtables for IUnknown and INumber, so start building CNumber2 by modifying CNumber1. Provide a constructor that takes the outer object's IUnknown interface (see Listing 16.12 and Listing 16.13), add a member variable to store that IUnknown interface (see Listing 16.12), and use that member variable to delegate QueryInterface, AddRef, and Release calls to the outer object (see Listing 16.14).
Listing 16.12 UMBER2.H--CNumber2 class CNumber2 : IUnknown { public: // IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); class ImpINumber : INumber { // IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); // ILrsInetUnlock methods HRESULT __stdcall GetNumber(double* pValue); HRESULT __stdcall SetNumber(double value); // A macro to gain access to the CNumber2 "this" // pointer from within the embedded class #define GET_CNUMBER2(pThis) \ CNumber2* pThis = \ ((CNumber2*)((BYTE*)this - \ offsetof(CNumber2, m_impINumber))); } m_impINumber; friend class ImpINumber; // Constructors and destructor CNumber2(); CNumber2(LPUNKNOWN); ~CNumber2(); private: ULONG m_cRef; double m_value; LPUNKNOWN m_pUnkOuter; };
Listing 16.13 UMBER2.CPP--
CNumber2::CNumber2(LPUNKNOWN) CNumber2::CNumber2(LPUNKNOWN pUnkOuter) { m_cRef = 0; m_value = 0.0; if(pUnkOuter == NULL) m_pUnkOuter = this; else m_pUnkOuter = pUnkOuter; }
Listing 16.14 UMBER2.CPP-CNumber2::ImpINumber::QueryInterface(), AddRef(), and Release() Are Delegated to pThis->m_pUnkOuter, Instead of pThis HRESULT CNumber2::ImpINumber::QueryInterface(REFIID riid, LPVOID* ppvInterface) { GET_CNUMBER2(pThis); return pThis->m_pUnkOuter->QueryInterface(riid, ppvInterface); } ULONG CNumber2::ImpINumber::AddRef() { GET_CNUMBER2(pThis); return pThis->m_pUnkOuter->AddRef(); } ULONG CNumber2::ImpINumber::Release() { GET_CNUMBER2(pThis); return pThis->m_pUnkOuter->Release(); } CNumber3: The Outer Object Now that you've created an aggregatable object that implements INumber, you create another object that implements IWholeNumber and uses aggregation to provide an implementation of INumber (see Listing 16.15).
Listing 16.15 INUMBER.H--The IWholeNumber Interface interface IWholeNumber : public IUnknown { public: // ILrsInetUnlock methods
virtual HRESULT __stdcall GetNumber( /* [out] */ long int* pValue) = 0; virtual HRESULT __stdcall SetNumber( /* [in] */ long int value) = 0; }; For simplicity, you're not going to make CNumber3 aggregatable, so declare the class with a single vtable for both its true IUnknown and the IWholeNumber interface. Add the member variable m_pUnkNumber to hold the true IUnknown of the aggregated CNumber2 object. You also provide an Init method so that the aggregation can be accomplished separate from the construction of the object (see Listing 16.16 and Listing 16.17). Modify the class factory so that it calls Init after constructing the object (see Listing 16.18).
Listing 16.16 UMBER3.H--CNumber3 Adds Init() and m_pUnkNumber class CNumber3 : IWholeNumber { public: // IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); // IWholeNumber methods HRESULT __stdcall GetNumber(long int* pValue); HRESULT __stdcall SetNumber(long int value); // Constructor and destructor CNumber3(); ~CNumber3(); BOOL Init(); private: ULONG m_cRef; LPUNKNOWN m_pUnkNumber; // Aggregated number };
Listing 16.17 UMFACT.CPP-CNumber3ClassFactory::CreateInstance Calls CNumber3::Init after Constructing the CNumber3 Object // Create the object CNumber3* pObj = NULL; pObj = new CNumber3(); IncrementObjectCount();
if(NULL == pObj) { _ASSERT(FALSE); DecrementObjectCount(); return E_OUTOFMEMORY; } // Call the initializer if(! pObj->Init()) { _ASSERT(FALSE); delete pObj; return E_FAIL; }
Listing 16.18 UMBER3.CPP--CNumber3::Init Creates the Aggregated Object BOOL CNumber3::Init() { _ASSERT(m_pUnkNumber == NULL); HRESULT hr = CoCreateInstance(CLSID_Number2, this, CLSCTX_INPROC_SERVER, IID_IUnknown, (LPVOID*)&m_pUnkNumber); if(FAILED(hr) || (m_pUnkNumber == NULL)) { _ASSERT(FALSE); return FALSE; } return TRUE; } CNumber3::Init creates the aggregated object by calling CoCreateInstance and passing its true IUnknown interface as an argument. CoCreateInstance will call the class factory's CreateInstance method, which will construct the CNumber2 object using the new constructor. CNumber2 now has the true IUnknown of the outer object (passed from CoCreateInstance to the class factory's CreateInstance method and then to the new constructor), which it stores as m_pUnkOuter. The true IUnknown of the CNumber2 object is returned back through CoCreateInstance, and the outer CNumber3 object has the true IUnknown of the aggregated object, which CNumber3 stores as m_pUnkNumber (see fig. 16.2). FIG. 16.2 CNumber2 and CNumber3 hold pointers to each other's true IUnknown interfaces. Now when CNumber3's QueryInterface is called, it will return this for IUnknown or IWholeNumber, but when INumber is requested, it will pass the call on to the aggregated CNumber2 object through that object's true IUnknown, m_pUnkNumber (see Listing 16.19).
Listing 16.19 UMBER3.CPP--CNumber3::QueryInterface HRESULT CNumber3::QueryInterface(REFIID riid, LPVOID* ppvInterface) { if(IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IWholeNumber)) { *ppvInterface = this; AddRef(); return NOERROR; } else if(IsEqualIID(riid, IID_INumber)) { return m_pUnkNumber->QueryInterface(riid, ppvInterface); } else { return E_NOINTERFACE; } } Aggregation and Tear-Off Interfaces in ATL ATL provides tools to implement aggregation. It also provides tools to implement a kind of temporary aggregation called tear-off interfaces. In each of these cases, the outer object declares the aggregated object in its COM_MAP through one of the COM_INTERFACE_ENTRY macros. Aggregation There are four steps to aggregating an object using ATL. First you make sure that the aggregated class has not explicitly denied aggregation. It can do this through the use of the macro DECLARE_NOT_AGGREGATABLE. If the ATL class doesn't explicitly deny aggregation, it's aggregatable. Second declare a member variable in the outer class to hold the true IUnknown of the aggregated object. If you are not using one of the automatic aggregation macros (COM_INTERFACE_ENTRY_AUTOAGGREGATE or COM_INTERFACE_ENTRY_AUTOAGGREGATEBLIND), you have to override FinalConstruct and create the aggregated object by using CoCreateInstance, like you did in the CNumber3 sample's constructor. When using the automatic macros, simply initialize the member variable to NULL. Third override the outer class's FinalRelease to release the aggregated object. This is true whether you use the automatic aggregation macros or one of the other macros. Finally declare the aggregation in the COM_MAP. The four macros listed below are available for doing this. The macro arguments are described in Tables 16.1 through 16.4.
COM_INTERFACE_ENTRY_AGGREGATE(iid, pUnknown) This macro is used to delegate a specific interface to an aggregated object.
Table 16.1 COM_INTERFACE_ENTRY_AGGREGATE Arguments Argument Meaning The ID of the interface that is to be delegated to the aggregated object.
iid
pUnknown The aggregated object's true IUnknown interface. The aggregated object is created in the outer object's FinalConstruct method COM_INTERFACE_ENTRY_AGGREGATE_BLIND(pUnknown) This macro is used to expose all of an aggregated object's interfaces from the outer object.
Table 16.2 COM_INTERFACE_ENTRY_AGGREGATE_BLIND Arguments Argument Meaning pUnknown The aggregated object's true IUnknown interface. The aggregated object is created in the outer object's FinalConstruct method. COM_INTERFACE_ENTRY_AUTOAGGREGATE(iid, pUnknown, clsid, cs) This macro is used to aggregate an object on demand, delegating the specified interface to that object. Table 16.3 COM_INTERFACE_ENTRY_AUTOAGGREGATE Arguments Argument Meaning The ID of the interface that is to be delegated to the aggregated object.
iid
pUnknown The aggregated object's true IUnknown interface. The outer object initializes this to NULL, and the first query for this interface creates the aggregated object. clsid
The ID of the class that is to be aggregated.
cs
A critical section used for synchronization.
COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND(pUnknown, clsid, cs) This macro is used to aggregate an object on demand, exposing all of the interfaces of that object.
Table 16.4 COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND Arguments
Argument Meaning pUnknown The aggregated object's true IUnknown interface. The outer object initializes this to NULL, and the first query for one of the aggregated object's interfaces creates the aggregated object. clsid
The ID of the class that is to be aggregated.
cs
This is a critical section used for synchronization.
Tear-Off Interfaces Tear-off interfaces are similar to aggregation, except that the inner object is held only until its interface is released. Remember that in aggregation, the aggregated (inner) object is released in the destructor, or in FinalRelease for ATL aggregation. This means that the aggregated object exists for the life of the outer object. Even using ATL's automatic aggregation, the aggregated object, once created, remains until the outer object is destroyed. Tear-off interfaces, on the other hand, are created when they're needed and destroyed when they're released. They're implemented differently from the way standard aggregation is implemented on both sides of the relationship. The tear-off interface must be an object specifically written to provide tear-off interfaces, and it can provide them only for a specific outer class. Now take a look at how it's done. First declare a new class that is derived from CComTearOffBase and the interfaces that this class will provide. To do this, the tear-off class must specify the owner, or outer, class. Listing 16.20 shows the declaration of a class that implements a tear-off interface.
Listing 16.20 Example Declaration of a Tear-Off Class class CTearOff: public ISomeInterface, public CComTearOffObjectBase { public: CTearOff() {} STDMETHOD(SomeMethod)() { return S_OK; } BEGIN_COM_MAP(CTearOff) COM_INTERFACE_ENTRY(ISomeInterface) END_COM_MAP() } Next the outer class declares the tear-off interface in its COM_MAP. The two macros listed below are available for doing this. The arguments to these macros are described in Tables 16.5 and 16.6. COM_INTERFACE_ENTRY_TEAR_OFF(iid, class) This macro is used to declare a true tear-off interface.
Table 16.5 COM_INTERFACE_ENTRY_TEAR_OFF Arguments Argument
Meaning
iid
The ID of the interface that is delegated to the tear-off interface object.
class
The class of the tear-off interface object.
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, class, pUnknown, cs) This macro is a variation of a tear-off interface in which the outer object caches the tear-off interface by holding its IUnknown interface. The outer object must release the tear-off interface in FinalRelease. This is functionally equivalent to automatic aggregation, except that it uses a tearoff interface instead of an aggregatable object.
Table 16.6 COM_INTERFACE_ENTRY_CACHED_TEAR_OFF Arguments Argument
Meaning
iid
The ID of the interface that is delegated to the tear-off interface object.
class
The class of the tear-off interface object.
pUnknown
The IUnknown interface of the tear-off interface object.
cs
A critical section used for synchronization.
You've looked at aggregation up close, and you've looked at how to do aggregation and tear-off interfaces with ATL. Next take a look at enumerators.
Enumerators: An Interface Pattern for Sets An enumerator is an interface that provides access to a series of elements and fits a specific pattern. The pattern is shown in Listing 16.21.
Listing 16.21 Enumerator Pattern interface IEnum : { STDMETHOD(Next)(ULONG celt, * rgelt, ULONG* pceltFetched); STDMETHOD(Skip)(ULONG celt); STDMETHOD(Reset)(void); STDMETHOD(Clone)(IEnum** ppEnum); }
An enumerator has four methods: Next, Skip, Reset, and Clone. Next gets the next celt elements in the enumerator. rgelt is the array of elements that is returned. The memory is provided by the caller and must be large enough to hold the requested number of elements. pceltFetched returns the number of elements that were fetched, which will always be equal to or less than celt. It will be less than celt when the number of elements from the current position to the end of the enumerator is less than the number of requested elements. Next returns S_OK when the requested number of elements are returned and S_FALSE when less than the requested number are returned. Skip moves the current position by celt elements. Skip returns S_OK when the requested number of elements are skipped and S_FALSE when less than the requested number are skipped. Reset returns the enumerator to its original state, with the current position at the beginning. Clone copies the enumerator in its current state. In this sample, you implement an enumerator for the class IDs of the four classes that you used to implement the INumber and IWholeNumber interfaces. Because class IDs are GUIDs, you implement the IEnumGUID interface, which is defined in MSDEV\INCLUDE\COMCAT.H. The enumerator class, CNumbers, will hold the class IDs in m_guids, and m_current will maintain the current position. CNumbers is shown in Listing 16.22.
Listing 16.22 UMBERS.H--The CNumbers Enumerator Class class CNumbers : IEnumGUID { public: // IUnknown methods HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* lpInterface); ULONG __stdcall AddRef(); ULONG __stdcall Release(); // IEnumGUID methods HRESULT __stdcall Next(ULONG celt, GUID* rgelt, ULONG* pceltFetched); HRESULT __stdcall Skip(ULONG celt); HRESULT __stdcall Reset(void); HRESULT __stdcall Clone(IEnumGUID** ppenum); // Constructors and destructor CNumbers(); ~CNumbers(); private: ULONG m_cRef;
GUID m_guids[4]; int m_current; }; In the implementation of Next, you return the requested number of class IDs from the current position, but not past the end of the array (see Listing 16.23).
Listing 16.23 UMBERS.CPP--Next HRESULT CNumbers::Next(ULONG celt, GUID* rgelt, ULONG* pceltFetched) { ULONG celtFetched = 0; for(ULONG ii = 0; ii < celt; ii++) { if(m_current < 4) { rgelt[ii] = m_guids[m_current]; m_current ++; celtFetched ++; } else break; } if(pceltFetched) *pceltFetched = celtFetched; if(celtFetched == celt) return S_OK; else return S_FALSE; } Notice that the return argument pceltFetched is optional. When it's NULL, the value isn't returned. Technically, the caller can send NULL only when a single element is requested. Skip and Reset are straightforward. Skip moves the current position forward, and Reset sets the current position to 0. Clone is a little different. It must create another object that is a copy of itself, including the current position. One way to do this is to call CoCreateInstance, which will go through the class factory so that object counts are handled there. However, CoCreateInstance has some overhead that isn't necessary in this case, so you do what the class factory does inside the Clone method; you construct the enumerator and increment the object count. Having created the object, use Skip to set its current position to the current position of the enumerator being cloned. The Clone method is shown in Listing 16.24.
Listing 16.24 UMBERS.CPP--Clone CNumbers* pNumbers = new CNumbers(); if(pNumbers == NULL) { _ASSERT(FALSE); return E_OUTOFMEMORY; } IncrementObjectCount(); HRESULT hr = pNumbers->QueryInterface(IID_IEnumGUID, (LPVOID*)ppenum); if(FAILED(hr)) { _ASSERT(FALSE); delete pNumbers; DecrementObjectCount(); *ppenum = NULL; return E_UNEXPECTED; } hr = (*ppenum)->Skip(m_current); if(hr != S_OK) { _ASSERT(FALSE); delete pNumbers; DecrementObjectCount(); *ppenum = NULL; return E_UNEXPECTED; } Any enumerator can be implemented pretty much the same way. Your internal data structure can be an array, a list, or anything else that has a logical order.
About the Samples The samples from this chapter are in the DLL project Number, described in Table 16.7. Table 16.7 Sample Source Code File
Contents
Number.mdp The project workspace. Number.mak The project makefile. inumber.h
The definition of the INumber and IWholeNumber interfaces.
numcid.h
The definition of the class IDs for each of the objects.
number.h
The definition of CNumber.
number.cpp
The implementation of CNumber.
number1.h
The definition of CNumber1.
number1.cpp The implementation of CNumber1. number2.h
The definition of CNumber2.
number2.cpp The implementation of CNumber2. number3.h
The definition of CNumber3.
number3.cpp The implementation of CNumber3. numbers.h
The definition of the enumerator sample class.
numbers.cpp The implementation of the enumerator sample class. numguid.cpp The local instances of the class IDs and interface IDs. numfact.h
The definitions of the class factories for all four classes.
numfact.cpp The implementation of the class factories and functions that are exported by the DLL number.def
The definition of the DLL exports.
number.reg
Manual registration file.
You can build the sample from the command line or from your IDE. After building number.dll, modify number.reg so that it contains the correct path to the DLL, and register the objects using regedit.exe number.reg or regedt32.exe number.reg. To execute the sample, you need an executable project. TestNum is a console application that serves that purpose. Build that project, and run TESTNUM.EXE from your debugger.
Distributed Component Object Model (DCOM) Microsoft has taken the Component Object Model, and therefore ActiveX, to another level with the introduction of the Distributed Component Object Model (DCOM). DCOM extends COM by enabling application objects to communicate and run across networks, including the Internet, intranets, LANs, and WANs. It allows your application objects to be distributed over multiple computers, handling the communications among the objects as well as the instantiation and execution of the objects remotely. The application uses the objects as if the application and the objects were running on the same machine. DCOM is Microsoft's answer to the Common Object Request Broker Architecture (CORBA). DCOM extends applications across networks, including the Internet, allowing components to run on different machines, by building on the Remote Procedure Call (RPC) technology. DCOM allows components to communicate with each other via any network protocol, including TCP/IP and IPX/SPX. DCOM gives you control of security features, such as access permissions and domain authentication, and can be used to launch applications on other machines. This capability enables
developers to develop truly distributed systems, without worrying about network programming, system compatibility, or integration of different components. Since ActiveX is based on COM, the language neutrality of ActiveX is extended to DCOM. ActiveX components built with different languages can communicate over a network. DCOM is designed to run on multiple platforms. Microsoft is openly licensing DCOM to other software companies to run on all major operating systems. Microsoft is also working with the Internet standards committees to make DCOM an Internet standard. To view a draft of this standard, go to http://ds1.internic.net/ds/dsintdrafts.html and search for DCOM. The exact address of the document changes as the version of the draft changes. DCOM is currently integrated with the Microsoft Windows NT 4.0 operating system and is in beta as an add-on for Microsoft Windows 95. It should be available for the Macintosh as a beta in the first quarter of 1997. Versions for different flavors of UNIX and Legacy systems, including mainframe systems running CICS and IMS, will be available sometime after that. UNIX support for DCOM will be provided by Digital, not Microsoft, through its ObjectBroker product. To find more information on the ObjectBroker product, look at the ObjectBroker Web site at http://www.digital.com/info/objectbroker/. To configure server and client applications to use DCOM on Microsoft Windows NT or Microsoft Windows 95, use the following steps: 1. Register the server application on both the server machine and the client machine. The server application does not need to reside on the client machine, although registering the server may involve running the server setup program, or running the server application, on the client machine. 2. If the server application uses custom interfaces, the marshaling code needs to be installed on both the server and client machines. 3. Server applications that support vtable binding need to have their type libraries installed on the server and client machines. 4. Alter the registry settings for the server on the server and client machines manually or by using the DCOMCNFG or OLE Viewer tool. DCCOMCNFG is available as part of the Microsoft Windows NT operating system and is located in the Windows NT System32 directory. The DCOMCNFG utility for Microsoft Windows 95 can be downloaded from the Windows 95 DCOM page of Microsoft's Web site (http://www.microsoft.com/oledev/olemkt/oledcom/dcom95.htm). The program used for installing DCOMCNFG on Microsoft Windows 95 will install the DCOMCNFG utility in the Windows System directory. To change the registry settings using DCOMCNFG, follow these steps:
1. Execute the DCOMCNFG.EXE file on the client machine. You will see a list of all registered applications in the Applications list box of the Applications tab. 2. If the name of your server is registered but does not appear, it is probably listed by its CLSID. To determine the CLSID of your server, you need to view the registry. To do this, close DCOMCNFG, and then open the Registry Editor by running regedit.exe. Open the HKEY_CLASSES_ROOT folder, and look for your server's ProgID. All the ProgIDs for all registered systems will be listed with their objects on the left. For example, if you were searching for MFCServer, you would look for MFCServer.Tracker. Click the + by the ProgId, and then select the CLSID subkey below it. The CLSID is displayed in the Data section of the right window. After you locate the CLSID, close the Registry Editor, and go back into DCOMCNFG.
CAUTION Use caution when looking at or editing the registry. If changes are not made correctly, they can cause your machine to function improperly and, in some cases, crash. You should always back up the registry prior to changing it. Information on backing up the registry can be found in the Registry Editor's help. 3. Select your server from the Applications list box, and then click the Properties button. This action brings up the Properties dialog. 4. Select the Location tab. Select the Run application on the following computer check box, and specify the name of the server machine. Clear the other check boxes, and then click the Apply button to save the changes. Close the Properties dialog, and then close DCOMCNFG. 5. Run DCOMCNFG on the server machine. Select your server's name (or CLSID) from the Applications list box, and then click the Properties button to bring up the Properties dialog. 6. Select the Security tab. You can use the default access and launch permissions or create custom permissions. 7. Make sure that SYSTEM is in the launch and access permissions. If you are using the default access or launch permissions, use the Default Security tab to make sure these permissions contain SYSTEM. 8. Add the user on the client machine to the access and launch permissions. To add users and groups to custom permissions, click the Edit button to bring up the Registry Value Permissions dialog. To add users or groups to default permissions, close the Properties dialog. Then select the Default Security tab, and click the Edit Default button in the Default Access Permissions or Default Launch Permissions frame to bring up the Registry Value Permissions dialog for the Default Access Permissions or Default Launch Permissions, respectively. Click the Add button to display the Add Users and Groups window. Select the user or group from the Names list box, and then click the Add button to add it. When you are done, click the OK button, and then
click the OK button on the Registry Value Permissions dialog. Click the Apply button on the Properties dialog to save your changes. Contact your system administrator if you need help setting permissions for users or groups. The OLE Viewer can be found on the Web at http://www.microsoft.com/oledev/olecom/oleview.htm, and it is included with the Microsoft ActiveX SDK in the Bin directory and with Microsoft Visual C++ version 5.0 in the Bin directory. The Web site usually contains the latest version. To change the registry settings using the OLE Viewer, follow these steps: 1. Run OLEVIEW.EXE on the client machine. Make sure Expert Mode is selected on the View menu. 2. Click the + beside All Objects in the left pane of the main window. All registered objects will appear under All Objects. If the name of your server does not appear, and it is registered, it is probably listed by its CLSID. If you need to find the CLSID of your server, refer to item 1 in the DCOMCNFG steps. 3. Locate your server by name or CLSID, and select it. This action displays a group of tabs in the right pane. 4. Select the Implementation tab, and clear the Path to the Implementation text box of the Local Server subtab. 5. Click the Activation tab, and enter the name of the server machine in the Remote Machine Name text box. 6. Close the OLE Viewer. 7. Run OLEVIEW.EXE on the server machine. Make sure Expert Mode is selected on the View menu. 8. Click the + beside All Objects in the left pane of the main window. Select your server's name or CLSID. 9. Use the Launch Permissions and Access Permissions tabs to set permissions for the server. You can use the default access and launch permissions or create custom permissions. 10. Make sure that SYSTEM is in the launch and access permissions. If you are using the default access or launch permissions, select System Configuration from the File menu to make sure these permissions contain SYSTEM. 11. Add the user on the client machine to the access and launch permissions. To add users and groups to custom permissions, click the Modify button on the appropriate tab to bring up the
Launch Permissions or Access Permissions dialog. Click the Add button to display the Add Users and Groups window. Select the user or group from the Names list box, and then click the Add button to add it. When you are done, click the OK button, and then click the OK button on the Launch Permissions dialog. To add users or groups to the default permissions, select System Configuration from the File menu to bring up the System Configuration dialog. Select the appropriate default permissions tab-- Default Launch Permissions or Default Access Permissions--and click the Modify button to bring up the Global Launch Permissions dialog or Global Access Permissions dialog, respectively. Click the Add button to display the Add Users and Groups window. Select the user or group from the Names list box, and then click the Add button to add it. When you are done, click the OK button, and then click the OK button on the Global Access or Global Launch Permissions dialog. You have some special considerations when using Microsoft Windows 95. First you need to install the DCOM add-on for Microsoft Windows 95. Information about downloading and installing it can be found on Microsoft's OLE page, http://www.microsoft.com/oledev/. To enable incoming calls, you will also need to change the EnableRemoteConnections setting in the Microsoft Windows 95 registry from "N" to "Y" on the server machine. This can be done using the OLE Viewer by selecting File, System Configuration, and then clicking the Enable Remote Connection (Windows 95 only) check box on the System Settings tab. DCOM on Microsoft Windows 95 does not support remote activation of a server because all processes run using the security of the currently logged-on user, making it impossible for a client machine to start a process. A server application running remotely on a Microsoft Windows 95 server machine will have to be started manually or some other way prior to the client machine accessing it; therefore, the launch permissions have no effect on Microsoft Windows 95. More information on DCOM, and other articles, can be found in the Knowledge Base article Q158582. Information can also be found on Microsoft's Web sites at http://www.microsoft.com/oledev/, http://www.microsoft.com/ntserver/, and http://www.microsoft.com/intdev/.
OLE DB Microsoft has introduced an object driven data access technology called OLE DB. OLE DB is a set of APIs that will provide ActiveX interfaces to all forms of data throughout the enterprise. OLE DB allows access to non-SQL type data as well as to SQL type data. Open Database connectivity (ODBC) drivers are used only to access SQL databases, such as DB2 and SQL Servers and still provide one of the best ways to access SQL databases. Currently, OLE DB provides only a layer on top of ODBC drivers for access to SQL databases; OLE DB does not access them directly. OLE DB will eventually be capable of accessing all types of data, including non-database data such as spreadsheets and e-mail. OLE DB, like ODBC, uses a common interface for accessing data. OLE DB has a modular design based on COM. The flow of an OLE DB and an ODBC application is the same, except for some slight differences.
Most of the differences are due to the fact that OLE DB is object-oriented, whereas ODBC is not, and because ODBC data is application-owned, whereas OLE DB uses shared data. Table 16.8 summarizes some of these differences. Table 16.8 OLEDB versus ODBC OLEDB
ODBC
Session Objects
Connection Handles
Shared data object
Application data buffer
Accessors
Descriptor Handles
OLE DB uses Session Objects in place of ODBC's Connection Handles. A separate Connection Handle is needed for each concurrent transaction. With OLE DB, an application can have several Session Objects per data connection, allowing one data connection for multiple concurrent transactions. OLE DB uses rowsets--in place of ODBC's result sets--which offer some advantages. ODBC reads data into an application's memory space for processing. OLE DB references data directly; data is not copied to the application's memory space. Referencing data directly saves on memory and processing time. If an ODBC application wants to know whether a second process or application has changed the data, the application must requery the data. If two or more objects are using the same data in an OLE DB application and one object changes the data, the other objects receive a notification that the data has changed. Basically, the memory buffer for the data is removed from the application and placed in a stand-alone shared data object. Applications access this shared data object using Accessors, which are similar to ODBC's Descriptor Handles. The application can use pointers to the data rather than an actual copy of the data and can share the data, providing quicker data access. An Accessor uses an array of binding structures. Each structure describes a column of data, so the array describes the entire table. The Accessor allows all needed columns to be bound at once instead of requiring repeated calls to SQLBindCol, allowing for more efficient binding. To find more information and to follow the development of OLE DB, check out Microsoft's OLE DB Web site at http://www.microsoft.com/OLEDB/. This site is one of the best sources of information for OLE DB. You can find the OLE DB SDK kit, white papers on OLE DB, and tools for OLE DB development. Programming magazines and other computer magazines are also a good source of information.
Threading Every process has one or more threads. A thread is code that is to be sequentially executed within a process. A process always has at least one thread, the primary thread, and can have multiple threads in addition to the primary thread. In a data entry routine, the primary thread might handle displaying the
data being entered while another thread updates the database once the data is entered. Threads can have different priorities. A thread with a higher priority can interrupt a thread with a lower priority. A thread will continue executing until one of the following happens: ● ● ● ●
The thread finishes executing its block of code. The thread is interrupted by a thread with a higher priority. The thread is interrupted by a user's action. The thread is interrupted by an operating system kernel's thread scheduler.
Each individual thread can run separate sections of code; one thread might perform different functions within a process. Multiple threads can run the same section of code. When multiple threads run the same section of code, each thread maintains a separate code stack. Separate code stacks prevent the threads from getting lost and tramping on each other. A process's global variables and resources are shared by every thread in the process. These global variables have to be used with caution; the values can be changed by another thread at any time.
Single-Threading and Multithreading In a single-threaded process, only one action can happen at a time. This approach was used in the older operating systems such as Windows 3.1. All incoming calls to the thread are received through the Windows message queue. Windows 95 and Windows NT introduced multithreading, which is more efficient than singlethreading. Multithreading allows an application to create more than one thread of execution so that process-intensive applications do not stall or freeze an application while waiting for the process to complete its execution. A single-threaded process will just wait until the action is complete. Multithreading presents some issues. It adds complexity to coding, testing, and debugging. Multithreaded applications must avoid deadlocks and races. Deadlocks happen when each thread is waiting for the other to do something, hanging the application. Races happen when a thread finishes before another thread it depends on finishes. Races cause a thread to use garbage data because the dependent thread has not provided legitimate values. Multithreading, in its simplest form, is referred to as the apartment model. A process that uses the apartment model uses multiple threads, but each COM Object lives in only one thread, or apartment, and cannot be directly accessed by other threads. A more sophisticated form of multithreading is the free-threading model. This model allows multiple threads to access each COM Object simultaneously. A multithreaded process can consist of one of these models or a combination of both. Apartment Model The apartment model is sometimes referred to as a single-threaded apartment model since it is a group of separate apartments or threads. Technically, all threading models use apartments. The single-thread-per-process model, referred to here as single-threaded model, is really one apartment for the entire process, whereas the apartment model is a group of apartments. Freethreading is sometimes referred to as a multithreaded apartment model since it consists of multiple threads in one apartment. For the purpose of this section, we will refer to single-threaded processes as single-threaded processes, and we will refer to multithreading processes as either apartment models or
free-threading models. The apartment model is a multithreaded process that contains only one COM Object per thread. The COM Object lives inside a group that is referred to as an apartment. All incoming calls are sent through the Windows message system. OLE synchronizes these calls, so the process can receive calls while making calls. Each thread has its own apartment, which is directly accessible by only one thread. Each apartment can receive direct calls only from the thread that belongs to the apartment. Call parameters need to be marshaled between apartments. OLE handles marshaling between apartments through the Windows messaging system. Free-Threading Model As mentioned earlier, a more sophisticated form of multithreading is the free-threading model. This model allows multiple threads to access each COM Object simultaneously. Free-threading is sometimes referred to as the multithreaded apartment model since the apartment contains multiple threads. A free-threading process is a multithreaded process that allows several threads to access a COM Object. Each COM Object is simultaneously accessible by more than one process thread. COM Objects are responsible for synchronizing incoming calls when using the freethreaded model; they must have their own message handlers. Calls are not passed through the Windows messaging system, nor does ActiveX synchronize the calls, since methods may be called from different processes simultaneously. Apartments cannot receive calls while making calls; asynchronous calls are converted to synchronous calls in free-threaded apartments. Objects must be able to handle calls to their methods from other threads at any time and to handle calls from multiple threads simultaneously. All threads are contained within a single multithreaded apartment. Since all threads reside in one apartment, there can be only one multithreaded apartment per process. Parameters are passed directly to any thread in the apartment. Data does not need to be marshaled between threads since all freethreads reside in one apartment. You need to make sure the process's code is thread-safe. Thread-safe means making sure the objects, data, and code owned by the thread are used by only that thread and not by other threads. If a multithreaded application is not thread-safe, the application will become confused and will not function properly. These problems can be difficult to debug. Mixing Apartment and Free-Threading Models Apartment and free-threading models can be combined within a single process. You can have only one free-threaded apartment, but you can have one or more single-threaded apartments. Interface pointers and other data must be marshaled between apartments. Calls to objects within single-threaded apartments will be synchronized with Windows messages, whereas calls to objects within the free-threaded apartment will not be synchronized at all. OLE threading models provide the support for the interaction between clients and servers with different threading models. As far as the calling object is concerned, all calls to objects outside the object behave the same, regardless of how the object being called is threaded. To the called object, the calls it receives behave identically, regardless of the callers threading model. Interaction between client and out-of-process servers is straightforward, whether their threading models are the same or different. The client and server are in different processes, and OLE handles the communication between these processes for you, using standard marshaling and RPC.
The interaction between clients and in-process servers present some issues. In-process servers do not call COM initialization routines, so you need to set the threading in the registry by adding the ThreadingModel named value to the InprocServer32 key of the server. You can set the threading manually, or you can use the OLE Viewer. Using the OLE Viewer to set the threading is as easy as setting the Threading Model on the Inproc Server subtab of the Implementation tab. The coding issues for in-process servers are too numerous and involved for the scope of this chapter. For detailed information on coding issues, see the "In-process Server Threading Issues" topic in the ActiveX SDK help files. Choosing a Multithreading Model Your decision about which multithreading model to use depends on the function of the object. For example, when creating a thread that interacts with a user, you may want to use the apartment model since incoming OLE calls can be processed with the Windows messages received. Because the apartment model is less complex, supporting it is easier than supporting the free-threading model. OLE provides synchronization through messaging for the apartment model, whereas the free-threading model needs to provide its own synchronization and storage for thread specific data. More information on threading is available. To learn more about threads in general, refer to the ActiveX SDK help files under "Processes and Threads." Specific coding help can be found in the ActiveX SDK help files and in the help files for Visual C++. The Knowledge Base on Microsoft's Web site contains information to help you understand basic threading in general, as well as specific coding information. Many third party books that discuss threading are also available.
Engineering for the Future For the past two years, Microsoft has been rapidly releasing new tools for ActiveX development. Visual Basic 5.0 brings significant changes to the VB world. VB can now create ActiveX controls, ActiveX documents, and a whole host of servers and components. The fact that VB now supports a native code compiler also makes it attractive. Visual Basic 5.0's language is now Visual Basic for Applications (VBA), so code written for Microsoft Office 97 products is the same as code written in native VB. Office 97 is a significant improvement over Microsoft Office 95. For starters, Microsoft Word now uses VBA rather than the nonstandard WordBasic. Using ActiveX controls in Office 97 is much easier than it was in Office 95. You basically select a registered control and drop it in the document, spreadsheet, and so on. Office 97 also has some VB improvements since it now uses VBA 5.0. A new visual language tool has arrived from Microsoft--Visual J++. Visual J++ is Microsoft's Java development tool. It offers a visual development environment complete with a compiler. Microsoft also offers a Java SDK on its Web site. Java SDK can be used with or without Visual J++ as a front end. The Java SDK includes access to the Windows APIs, allowing developers to create sophisticated Windows systems.
Along with the languages used to build applications, advances are being made in handling the data. Microsoft has released an object-driven data access tool called OLE DB. OLE DB is a set of APIs that provide ActiveX interfaces to all forms of data throughout the enterprise. Along with providing access to databases, OLE DB will provide access to such things as text in an e-mail or spreadsheet. Distributing data over the Internet and an intranet is getting easier. Microsoft's Advanced Data Connector (ADC) allows the developer to create applications that interact with databases over the Internet or an intranet. ADC allows you to cache data on the client machine, manipulate that data, and integrate that data with data-aware ActiveX controls. Microsoft has also developed a technology for distributing applications over networks, including the Internet and an intranet, called Distributed Component Object Model (DCOM). DCOM extends ActiveX by enabling application objects to communicate directly over networks. To make the development, deployment, and management of server applications over a network, intranet, or the Internet, Microsoft has released Transaction Server. Transaction Server insulates the developer from dealing with system issues such as connectivity, security, thread management, and data management. For the design, development, and management of a Web site, Microsoft offers FrontPage. FrontPage provides Web site developers with the tools needed to completely develop a Web site. Microsoft offers another package, ActiveX Control Pad, for developing individual Web pages using a form approach similar to VB. This package is a good way to quickly test ActiveX controls for the Internet. For developing Web applications, Microsoft offers Visual InterDev. Visual InterDev provides a visual development environment that includes the tools necessary for developing a Web application in its entirety. Microsoft is trying to develop solutions to leverage companies' existing knowledge base so that developing ActiveX components has a shorter learning curve. Microsoft is upgrading its existing tools so the tools can be used for ActiveX development. When Microsoft releases new tools, those tools have the look and feel of a company's existing tools. Companies that have expertise with Microsoft products will find the task of moving to ActiveX easier. Table 16.9 lists the Web addresses that provide information on the products mentioned. To keep on top of Microsoft's new and updated products, you can frequently visit Microsoft's Web page at http://www.microsoft.com, especially the Internet developer page, http://www.microsoft.com/intdev/, and the Microsoft Developer Network(MSDN) page at http://www.microsoft.com/msdn/. MSDN CDs are also a good source of information.
Table 16.9 Where to Find Information on the Products Discussed Product
Address
Visual Basic
http://www.microsoft.com/vbasic
Office 97
http://www.microsoft.com/office
VBA
http://www.microsoft.com/vba
Visual J++
http://www.microsoft.com/visualj
Java SDK
http://www.microsoft.com/java/sdk
OLE DB
http://www.microsoft.com/oledb
ADC
http://www.microsoft.com/adc
DCOM
http://www.microsoft.com/oledev
Transaction Server
http://www.microsoft.com/transaction
FrontPage
http://www.microsoft.com/frontpage
ActiveX Control Pad
http://www.microsoft.com/workshop/author/cpad
Visual Interdev
http://www.microsoft.com/vinterdev
ActiveX
http://www.microsoft. com/activex
From Here... ActiveX is a constantly changing world. New technologies and tools are coming out faster than any one person can keep up with, let alone an entire industry. Not only are you responsible for creating sound software, you also are now responsible for its interaction with other components, applications, and computers. Unfortunately, we have no easy answer for how to keep up with the onslaught known as ActiveX. Microsoft does publish a huge volume of information about new technologies and tools on its Web site on the Internet. Also, the Internet newsgroups are especially helpful in locating resources (human and digital) to assist you. Hundreds of developers every day access the forums, exchanging information about ActiveX development. When it comes to ActiveX development, the only advice that we can give you is to be patient, take your vacation time when you've earned it (believe me, your work will be waiting for you), and study a lot. Don't worry if you don't have all the answers; no one does. We truly hope that you gain as much from this book as we did in writing it.