In our examination of object-oriented application frameworks and class libraries, we asked the experts to show us the best way to use their tools--we wrote the spec, they wrote the code. Like us, you'll be surprised at some of the results.
October 01, 1992
URL:http://www.drdobbs.com/cpp/sizing-up-application-frameworks-and-cla/184408851
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
Copyright © 1992, Dr. Dobb's Journal
f
This article contains the following executables: DDJAFX.TXT (program spec) BOROWL.ARC (Borland OWL) OWLYAO.ARC (OWL version by Paul Yao) INZAPP.ARC (Inmark zApp) OBJMNU.ARC (Island Systems object-Menu) CPPVUS.ARC (Liant C++/Views) MSMFC.ARC (Microsoft MFC) .
Ray is senior technical editor at DDJ. He can be reached through the DDJ offices, at 76704,51 on CompuServe, or at [email protected].
As today's applications increase in complexity, so do the tools for building them. Application frameworks, class libraries, and GUI toolkits are three such categories of tools, yet they represent very different technological approaches. Still, the tool categories, though different, all address the same basic software-development issues: reducing coding effort, speeding time to market, increasing maintainability, adding robustness, and leveraging prefab functionality.
Although comparing these tools is like comparing apples, oranges, and bananas, when you start a development project, you might end up saying, "All these products look like fruit to me." In this article we'll provide you with a basis for deciding which of these approaches is right for you, focusing on five object-oriented application frameworks and class libraries. In future articles, we'll cover GUI toolkits not based on object-oriented languages, as well as additional class libraries.
When your project is at square one, there's a simple question: Do you stay with the raw API (basic DOS or Windows services), or do you choose something at a higher level? If you choose the latter (the "fruit scenario"), then you're at square two: choosing between an application framework, a class library, or a GUI toolkit.
Once you decide on one of these three categories, the next question is, which particular toolkit? Your decision can be as critical to your success as your choice of algorithm or programming language. These categories are new enough that no clear standards have emerged, and many plausible contenders exist. Even properly defining these categories is sticky. (See the textbox, "What is an Application Framework?")
The trade-offs are numerous. With an application framework, you can cruise above the API at a high altitude, but you'll have the steepest learning curve. Their productivity-increasing power implies great complexity. Also, because the framework constrains your design decisions, you run the risk of crashing into a brick wall. If you use a GUI toolkit, you stay much closer to the ground-level API and enjoy a gentle learning gradient as well as free choice in architectural-design approach. The downside, of course, is that you must do much more of the work yourself. Smack-dab in the middle between GUI toolkit and application framework is the class-library category.
This is where we found ourselves when we needed a program to display digitized samples for our "Handprinting Recognition Contest." We chose the safe but slow ground-level route of writing the program using the native Windows API. (Actually, we first wrote a DOS version which used the compiler's library of rudimentary graphics primitives.) Like programmers everywhere, we had a deadline and were leery of diving into a large, complex product we had no experience with. In retrospect, we wondered which tool was the right one for the job; hence this article.
Because much of an application framework's machinery lies under the surface, evaluating application frameworks is a uniquely difficult task. A few hours spent with the manual or looking over example programs won't cut it for complex and subtle tools.
So how do you make an informed choice? Not by reading most product reviews we've seen. The limited hours allotted for a standard product review--typically, an evening with the manual, plus a weekend working with sample code--do not give the reviewer sufficient exposure to realworld use of the framework. (A recent review of several application frameworks in another programming magazine presents, with a straight face, the source code to "Hello World" using four different products, averaging about a dozen lines of code each. Although "Hello World" is a convenient way to meet a framework, it's not something on which you want to build a long-term relationship. It's like buying a house based on the look of the front door.) As with a programming language, the best way to evaluate a tool is to live with it for a while on a nontrivial development project.
But the live-in approach has problems too. First, the steep learning curve precludes conducting more than one or two such evaluations. Although the products are meant to make life simpler, they are complex pieces of software machinery, as Table 1 and 2 show. Second, weeks or even months can elapse before you run into product limitations that stymie further development or require contrived workarounds. When you finally discover that your framework is in fact a straight-jacket, it may be too late. A third problem is that, even if a framework is well designed and bug free, it may offer multiple ways of accomplishing a given task, and it is not always clear to the novice which is the best approach. By contrast, the experienced user of a framework knows which methods offer best performance and are easiest to code, but such knowledge is often hard-won.
Number of classes -------------------------------- Apple MacApp 3.0 70 Borland OWL 75 Digitalk Smalltalk/V 110 Go PenPoint 250 Inmark zApp 121 Island object-Menu 81 Liant C++/Views 102 Microsoft MFC 87 Think C Class Library 61 Xerox Smalltalk-80 120
Lines of Source in Code Kilobytes ------------------------------------------- Borland OWL + ClassLib CPP files 11,300 332K HPP files 13,678 411K Total 24,978 743K Inmark zApp CPP files 19,299 612K HPP files 3,802 100K Total 24,101 712K Liant C++/Views CPP files 27,000 600K HPP files 10,139 275K Total 37,000 875K Microsoft MFC CPP files 17,202 403K HPP files 8,211 285K Total 25,413 688K Island object-Menu CPP files 35,268 1150K H files 6,484 250K Total 41,752 1400K
The tack we settled on was to ask expert programmers at various tool vendors to code up the same small, but non-trivial example program. We wrote a program specification (see the textbox, "The Spec for the Sample Application") that exercised many of the most interesting features of these different packages, yet can be implemented by an expert user of a framework in three or fewer working days. Someone new to the framework, by contrast, could spend a couple of weeks accomplishing the same task.
By examining different implementations of the same functional requirements, you can obtain concrete information to help you decide which available tools are appropriate. You can then weigh the resulting programs in light of your own particular trade-offs. The program specification, programmer's notes, complete source code, and executables are available electronically; see "Availability" on page 5.
This isn't a confrontational "shoot-out." Arguing about the "best" framework is like arguing about the best cuisine, text editor, or programming language. Our position is that different tools satisfy different needs and tastes: Some support portability between platforms, others focus on high-level object orientation, others emphasize closeness to the Windows API, and still others swear by performance optimization. Every programmer has a unique set of opinions about which characteristics are important and which can be sacrificed in real-world trade-offs. Not only that, but strongly held opinions often change in the course of development, in response to changing market conditions.
We find it more interesting to get a real-world useful application that works, rather than a contrived example that displays 20 different dialog boxes that we'll never use. With this in mind, our specification offers plenty of leeway to the implementor to solve the problem in a way best suited to the application and tool. This wide latitude in design choices reflects the implementations described here: We had no dogmatic preferences about UI widgets; we just cared about getting the job done. Our philosophy is that any program is better than no program, and the world's fanciest empty dialog box is useless compared to a complete rudimentary program that accomplishes the given task. In the case of our own implementations (the raw Windows API and the DOS versions), the results are indeed homely but usable nevertheless.
Our focus is on the single-user, graphics-oriented application. The example does not do anything related to text-editing, multiform data entry, database access, or multiuser processing. Nor does our spec address cut, copy, paste, selection by pointing, raster-image data, and the all-important Undo command. In homage to real-world randomness, we'll leave these for another time.
We've not tried to be comprehensive in our selection of vendors. (There are way too many to cover in one or two issues of the magazine.) Rather, our focus remains on technology issues rather than specific products. We've tried to make our list representative rather than comprehensive. Comparing apples, oranges, and bananas is a stated goal; comparing all such instances is not.
Although the boundaries are not always clear-cut, we look this month at object-oriented application frameworks and class libraries. Future articles will focus on GUI toolkits and bare-bones class libraries.
This article sizes up the following tools:
The results are indeed striking in both diversity and homogeneity -- a fascinating case study in user-interface design as well as engineering trade-offs. Figure 1 through Figure 5 show screen displays from the various implementations. Table 3 summarizes which features were actually implemented. Tables 4, 5, and 6 provide some rudimentary metrics (lines of code, executable size, and runtime memory profile, respectively) on the various implementations. These metrics, like EPA mileage estimates, are for rough comparisons only; relative standings may vary depending upon your application.
Borland Inmark Island Liant Microsoft OWL zApp object-Menu C++/Views MFC ------------------------------------------------------------------------- File-open dialog x x x x x Data window is resizable -- x x -- -- Data window is scrollable -- -- x -- -- Access commands via menu x x x x x Access commands via toolbar or button x x x x x Show menu help in status pane -- x x -- -- Show general help in Help window -- x -- x -- Select instance by pointing x x x x x Select letter by pointing x x x x x Select instance by keyboard x x -- -- -- Select letter by keyboard x x -- -- -- Select instance by scrollbars x x x x x Select letter by scrollbars x x x -- x Show all letters and instances -- -- x -- -- Multiple kinds of views of instance x x x -- x Display custom sequence of letters -- -- x -- -- Letters in drop-down graphic list -- -- -- -- x Change line color of letter x x x x x Change background color of letter x x x -- x Change line width of letter x x x x x Change scaling of letter x x -- -- -- Print letter x x x x x MDI-style child windows -- -- x -- -- Tear-off menus -- -- x -- --
Line Borland Inmark Island Liant Microsoft Type OWL zApp object-Menu C++/Views MFC --------------------------------------------------------- CPP 2470 1116 1592 1512 412 HPP 269 -- -- -- -- C -- -- -- -- 251 H 816 29 197 334 175 RC 106 74 -- 5 85 DEF 6 9 -- 8 11 ____________________________________________ Total 3398 1497 1789 1859 934
Borland OWL 56,848 Inmark zApp 334,848 Island object-Menu 470,048 Liant C++/Views 269,312 Microsoft MFC 46,536
Segment Type Number of Number Segments of Bytes ----------------------------------------------------------- Borland OWL Code 11 24,928 DGroup 1 22,304 Resource 2 1,536 Private data 3 4,896 Total Application 17 54,784{*} Inmark zApp Code 17 208,352 DGroup 1 39,328 Resource 2 1,024 Private data 2 69,632 Other 10 2,176 Total Application 32 320,512{*} Liant C++/Views Code 14 197,792 DGroup 1 51,104 Resource 9 5,248 Private data 5 69,952 Other 2 1,024 Total Application 31 325,120{*} Microsoft MFC Code 1 29,984 DGroup 1 15,520 Resource 8 992 Other 3 1,280 Total Application 12 47,776{*}
{*} The OWL implementation has an additional runtime requirement of 312,640 bytes for the OWL runtime DLLs. In addition, the OWL, zApp, C++/Views, and MFC implementations are all Windows-based programs, and therefore require memory consumption of 915,584 bytes of Windows runtime environment. Because Island Systems' implementation is DOS-based there are no comparable figures; after program load, the memory manager reports 346 Kbytes available for use by the application.
The following sections discuss, in alphabetical order, each of the five toolkits and their corresponding implementations. While there's not enough room in this article to fully describe any one of the five products covered here, we'll introduce them briefly and touch upon interesting highlights. Keep in mind that our goal is not detailed coverage, but rather to direct your attention to areas for further investigation.
The tools in this article all run on the PC platform. Four of the five packages--Borland's OWL, Microsoft's MFC, Inmark's zApp, Liant's C++/ Views--are Windows-based, while Island's object-Menu is DOS-based. Others, such as Glockenspiel's CommonView and Autumn Hill's Menuet/CPP, are certainly worth considering. Fortunately or unfortunately, the area of application frameworks and class libraries is one in which there will be many available choices and little consensus of opinion about which is best, for some time to come.
OWL from Borland, while not the first framework on the PC market, is certainly the most visible and likely the one with the most number of installations, given the popularity of Borland's C++ language products.
The technology in OWL evolved from an earlier effort by the Whitewater Group (authors of the Actor language/environment for Windows), which has been in use for some years and is generally well regarded. OWL relies on a nonportable extension to C++ known as dynamic-dispatch virtual tables (DDVT), which simplifies the handling of Windows messages. While Borland has shown a version running on NT and has announced plans to support OS/2, OWL currently runs only on Windows. Note that OWL does not support DOS apps; for that, you must use Borland's companion framework, TurboVision.
OWL classes (of which there are 25, plus 405 methods) cover the middle layer of user-interface components, such as dialogs, controls, and text-edit windows, as well as some higher-level components (application classes, MDI frame windows). Graphics classes (those that correspond to the Windows GDI) can be found in the Whitewater Group's ObjectGraphics library, designed to complement OWL. OWL does not provide low-level components such as data-structure classes (collections, sets, arrays, lists) and date and time classes. These low-level classes are part of Borland's Class Library, which runs on both DOS and Windows and supports both OWL and TurboVision. Borland's Class Library comes with the OWL package, and for this reason we count OWL and the Class Library together in our discussion and in the table summaries. (Windows expert and noted author Paul Yao has ported our DOS implementation to Windows using OWL, resulting in an implementation of 1050 lines. This implementation is also available electronically, if you want to see another approach to OWL programming.)
Figure 1 shows the interface Borland programmers designed. Listing One (page 106) shows some of the code implementing the main application window. Borland's implementation of our sample application required the most lines of code; see Table 4. Despite the number of lines, the resulting executable is still small; see Table 5. Unlike those implementations with large executables, programs written with OWL do require about 312K of runtime DLLs. The runtime support, however, is shared by all OWL applications running at the time, much as Windows itself is shared by Windows apps.
Of the Windows-hosted products discussed here, zApp and C++/Views are the two which strive to take the high road above the Windows API. zApp and C++/Views each define higher-level abstractions that offer a modern, event-driven GUI application model, which is portable to platforms other than Windows (OS/2, DOS, and Motif). The advantage of the "high-road" approach is that the design is not burdened or warped by the historical vagaries of the Windows API. The disadvantage for the programmer is that there is a brandnew set of concepts to learn, with the potential danger that some of these abstractions may be poorly conceived and not stand the test of time.
Unlike C++/Views, which is strongly influenced by the classic Smalltalk MVC paradigm, zApp has its own unique design. The design, however, results from years of experience in building GUI apps. (The architects of zApp started out as a consulting firm doing GUI software development in C++.)
There's a saying that good design is invisible. As Ward Cunningham puts it, "Good class libraries whisper the design in your ear." This is the case with zApp, which straight-forwardly provides all the usual classes you'd expect in building event-driven GUI programs: a main application class, a small hierarchy of event-handling classes, classes for graphic display, window-containing classes, and the usual GUI widgets (push button, check box, list box, combo box, and so on). There are no radical concepts or unpleasant surprises, greatly easing any learning curve associated with a brand-new API.
Inmark's implementation is shown in Figure 2. Listing Two (page 108) shows some of the principal member functions in Inmark's implementation of our sample application.
object-Menu is the only DOS-based product we cover in this article. Like the other packages here, it's written in C++ and provides a set of classes that serve as a configurable framework for a GUI application. Island Systems plans to release a Windows version later this year.
The programmers at Island Systems came up with the most complete implementation of our sample application (see Table 3), showcasing many of the features in their product. The application's visual components (see Figure 3) have a sculpted, three-dimensional look reminiscent of Motif. One slightly disconcerting element for people used to Windows (and the Mac) is that the mouse pointer arrow angles to the right rather than to the left.
Among the features of the sample implementation, multiple scrollable browser windows can be opened to view part or all of the handwriting-data samples. Listing Three (page 110) shows excerpts from Island's implementation. Each window can be iconized, and has its own menu. An object-Menu window includes functionality for maximize, minimize, close, resize, drag, and autoplacement of internal items. object-Menu can use automatic placement directives to define positioning of visual components, similar to the facility in zApp and C++/Views. Unlike other implementations that use scrollbars to select next and previous characters in the alphabet, the scroll bars here truly scroll the items in a given window's display.
One nice feature in Island Systems' implementation is a window that allows you to type in a string and see the corresponding letters from the handwriting sample data. Another unique feature is the use of tear-off menus, similar to those on the Mac and in OpenLook; the Edit/Copy menu is the only tear-off menu in this implementation. Island's implementation apparently does not directly support printing, but accomplishes the task via the use of Graf-Drive Plus (from Fleming Software). Note also that object-Menu does not contain graphics primitives, but can work with a number of low-level graphics libraries (Borland's BGI, MetaWindow, Genus FX, and Flash Graphics).
The executable size of Island Systems' implementation was larger than the others. However, considering that the other packages are relying on Windows for extensive support (approximately one megabyte of runtime support, up to five megabytes on disk), the size of the Island Systems implementation does not seem so large.
With regard to performance, all of the Windows-based implementations felt about the same on our test hardware (a 386/33 with 8 Mbytes of RAM). Island's DOS-based implementation was the only one that seemed sluggish. One reason for this is that the other implementations were running in Windows Enhanced mode, which uses both extended and virtual memory. In DOS, of course, any memory above 640K generally lies unused. Because the code we supplied reads the entire data file (250K worth) into memory, there's not much room left, and the overlay manager must kick into action. Using a disk cache helped, but the sluggishness remained. object-Menu can get around this overlay swapping by using Phar Lap's DOS extender and Metaware's 32-bit C++ compiler, but we did not try this version.
Another way to speed up performance might be to use an optimized graphics library, such as MetaWindow, or Flash-Graphics instead of BGI.
Of the five tools covered here, Liant's C++/Views may have the longest heritage. C++/Views evolved from a C-based package that attempted to bring to C some of the benefits of the Smalltalk language and class libraries. The package was called "c_talk" (from CNS, later acquired by Liant) and came out in 1988. It was noteworthy because it included a class-browser utility similar to that found in Smalltalk-80 and Digitalk's Smalltalk/V. A class browser is a program that allows you to view and edit the source code for classes by using a multipane window display. In one window pane is a scrolling hierarchical list of classes, on a second pane is a scrolling list of methods (termed "member functions" in C++), and in a third pane is the code for the method itself, which can be edited and changed.
The design of the class browser stems from the object-oriented nature of the Smalltalk environment, in which there is no real concept of source files, header files, or module linking as there is in C and C++. By contrast, the integrated development environments (IDE) from Borland and Microsoft are designed with the individual source-code file as the main focus of a programmer's attention. As time passes, these IDEs have evolved to better support object-oriented programming by supplying graphical views of class hierarchy and so on. In the DOS or UNIX C/C++ environment, you can always drop down out of the IDE to work at the command-line level, editing source files with your favorite text editor and running the compiler directly (or via make). In the Smalltalk environment, the class browser provides the sole access to the source code. Liant's version allows you to work in both ways. Source code is maintained in source files, rather than in a persistent object database of classes and methods.
C++/Views is strongly influenced by Smalltalk's Model/ View/Controller (MVC) paradigm. This approach to structuring applications consists of three parts: a control component that manages events, a view component that manages the presentation of data to the user, and a model component that encapsulates application-specific data and processes. (See Adele Goldberg's "Information Models, Views, and Controllers," DDJ, July 1990.) According to this protocol, any changes made to the application's data model (say it's a collection of numbers) can be automatically reflected in multiple views (such a bar chart, pie chart, or spreadsheet view) without the model knowing which views are presenting the data. In my opinion, this design strategy has not been surpassed in the 15 years since it was first conceived.
Despite this heritage, C++/Views is more than a clone of Smalltalk-80; it has its own design. The VNotifier class is the controller that handles events. The VView class (a subclass of VWindow) is an abstract class from which view classes are derived. Graphics output is accomplished through the VPort class. The application's data model can be constructed using user-defined classes in conjunction with C++/Views data-structure classes (sets, dictionaries, lists, and the like).
Liant's implementation of our sample application is not the most feature-filled, but has, as Figure 4 shows, a clean, intuitive feel. The implementation strategy decomposes the main window into a nested set of panes, or views, corresponding to the various visual elements on the application. Listing Four (page 111) shows a sample of the browser code. One nice feature in Liant's class library is the VRatio class, which is used in conjunction with the VFrame class (a window frame) to flexibly specify the screen size of display objects. This is similar to the automatic sizing facility in zApp.
Most articles on Microsoft's MFC say it's a "thin veneer" or "wrapper" around the Windows native API. This myth is not substantiated by the facts. Metrics such as line count, number of classes, and number of methods indicate otherwise.
The reason behind the myth is likely that MFC is designed to sit at a close conceptual distance from the Windows API. Classes in MFC map on a one-to-one basis with entities in the Windows API, such as display context, bitmap, brush, button, MDI child window, and so on.
In terms of lines of code, the source for MFC library is larger than the source for OWL or zApp (although all are in the same general ballpark). Those lines of MFC source code are not just idly sitting there: Microsoft was able to implement our sample application using the fewest lines of code. In addition, Microsoft's implementation had the smallest executable and required the least amount of runtime support from DLLs. Listing Five (page 113) shows sample code that makes up the browser UI.
Microsoft's implementation was not as comprehensive as some of the others (such as Inmark's or Island Systems'), although it did fully satisfy the specs we provided. Microsoft's implementation (Figure 5 shows the UI implemented by Microsoft) provided one nice feature that the others did not: a drop-down list box that shows the graphical shapes of the handwriting data. Using this UI component as a means for selecting letters, rather than having to handcode the tabular display of shapes, may have contributed to the compact implementation.
One possible reason for the small executable size is the use of a dialog box as the main application window, rather than using a "heavy-weight," user-defined window. In Windows, dialog boxes are built-in components, in that they don't require user code for registration or full-fledged window procs, thus saving space in the executable and/or the runtime. Using dialogs means the main window is not scrollable or resizable (not required by the spec). Note that other implementors also used the dialog-box technique (in Borland's implementation, for example, TMainWindow is a subclass of TDialog), but without equivalent savings in space.
Microsoft has demonstrated MFC on DOS text mode (via Mewel, like Inmark's zApp), on Windows/NT, and apparently has plans to create a portability layer for the Macintosh platform (according to leaks published in the trade press). Inelegant yes, but portable nonetheless.
The design of MFC requires a form of exception handling and template classes, which are implemented in a nonstandard way due to deficiencies in Microsoft's C7 compiler (unlike Borland's compiler, C7 does not support templates). But these mechanisms are implemented using macros and other standard constructs, and are therefore theoretically portable to other compilers. It would be an interesting exercise to port MFC to Borland's compiler. For those who don't mind sitting within spitting distance of the native Windows API, such a combination may well be the best of both worlds.
All the tools discussed here were able to deliver on our program spec. Each has different strengths and weaknesses; you'll have to decide which one fits your requirements profile. Try out the sample implementations. Ask the vendors for additional information and demo diskettes.
Although these products are full-featured and mature, it seems that none of the PC-based products yet contain the rich functionality present in non-PC-based frameworks like MacApp or PenPoint. This situation will likely change. In the meantime, you can still get a leg up on program development by using one of these products.
Although application frameworks are not a new concept, they have only recently arrived as a mainstream development tool on the PC platform. Object-oriented languages are ideal vehicles in which to embody an application framework, and the advent of C++ to the PC platform has allowed mainstream PC programmers to finally enjoy the benefits of application frameworks.
From the early '80s to the start of this decade, C++ was found mostly on UNIX systems and researcher's workstations, rather than on PCs and in commercial settings. C++, along with other object-oriented languages, enabled a number of university and research projects to produce the precursors to today's commercial frameworks and class libraries. The most visible of these early efforts were Mark Linton's InterViews, Keith Gorlen's NIH class library, the Andrew toolkit from CMU, and Erich Gamma's ET++ framework.
These tools had their roots in the Smalltalk-80 system, the granddaddy of frameworks and class libraries. In Smalltalk-80, there's no concept of an application distinct from the enclosing framework or environment. The act of programming in Smalltalk consists of navigating the class hierarchy in search of appropriate classes to reuse or subclass. And, it turns out, there are quite a few. The typical Smalltalk-80 environment has over 120 classes, 4000 methods, 6000 instantiated objects, and 1.3 Mbytes of source code.
In 1985, Apple Computer's MacApp system rigorously systematized the key ideas of a commecial application framework: a generic app on steroids that provides a large amount of general-purpose functionality within a well-planned, well-tested, cohesive structure. More specifically, Apple defines an application framework as "an extended collection of classes that cooperate to support a complete application architecture or application model, providing more complete application development support than a simple set of class libraries." This support means not just visual components such as list boxes and dialogs, but all the other facilities needed by an application, such as support for undoing commands, printing, and debugging.
For our purposes, we define an application framework as an integrated object-oriented software system that offers all the application-level classes (documents, views, and commands) needed by a generic application. An application framework is meant to be used in its entirety, and fosters both design reuse and code reuse. An application framework embodies a particular philosophy for structuring an application, and in return for a large mass of prebuilt functionality, the programmer gives up control over many architectural-design decisions. The architectural approach used by many application frameworks is an evolutionary descendant of Smalltalk's Model/View/Controller triad.
Class libraries and GUI toolkits are generally smaller and simpler systems than application frameworks. We define a class library as an object-oriented set of workhorse classes that can be incorporated piecemeal into an application, rather than the other way around. (Apple's definition of a class library is "a collection of classes designed to work together to make a given set of programming tasks easier, i.e., numerics, graphics memory management, etc.") By allowing the programmer to pick and choose, a class library does not enforce any particular architectural approach. Class libraries are object-oriented implementations, unlike GUI toolkits. The services offered by class libraries include both user-interface functions (menus, dialog boxes, and graphics primitives), as well as general-utility functions (date and time conversion, data-structure manipulations, and so on).
Finally, GUI toolkits offer services similar to those of a class library, but using a procedure-oriented rather than an object-oriented interface. GUI toolkits predate object-oriented languages, and some of the most successful products in this category are written in assembler. Most DOS-based GUI libraries are at the same basic level of abstraction as Windows (or at least the early versions of Windows). Unlike class libraries, GUI toolkits generally do not offer nongraphical utility routines (queue management routines, for example).
Both class libraries and GUI toolkits often come bundled with design tools that allow you to interactively specify application components, such as dialog boxes and menus.
In practice, the distinctions between one product and another are not as clear-cut as these definitions. For example, one class-library vendor directly compares their product against a popular application framework. Another class-library vendor avoids comparisons with application frameworks, yet the sample programs bear an uncanny philosophical resemblance to those of the application framework.
Although the ancestral lines are often blurred, a collective unconscious (community memory) is at work here. For example, one particular vendor (not part of this issue's roundup) was unaware of the basic concepts of MacApp, Smalltalk, and the MVC triad, even though his product competes directly against frameworks that embody the architectural heritage of these OOP pioneers.
--R.V.
We've all been there. Your boss comes to you on a Friday afternoon with a request for a "simple program" that's needed ASAP. In this particular case, it happens to be a graphics-oriented program to display some digitized vectors.
"It's real easy," your boss tells you. "All you have to do is let the user pick from a list of files, then read in the data, which is just a bunch of points, and then display those points using MoveTo and LineTo. Do you think you can have it ready by Monday morning?"
This, in essence, is what we asked the tool vendors to do. The application is called HWX Browser, and it allows users to read in different sets of handwriting data from files on disk and display the alphabets in an application window.
The program is not a contrived example meant to exercise items on vendor's feature lists. Rather, the specs come from an entirely different project and predate the current effort. The program is in many ways representative of graphics-oriented programs, as well as offering its own unique challenges to the implementor.
The DDJ HWX Browser allows the user to view handwriting samples which have been gathered from a number of people. Each file represents samples collected from a single person, in the form of a series of alphabets. The file has a particular structure, consisting of a short header, followed by the data for individual letters of the alphabet, each letter being represented by multiple instances or versions. The data for a single instance is basically a sequence of coordinates as gathered by the digitizer; a set of points for each stroke in a character.
Each vendor received the same specs and had the same amount of time. The basic areas of functionality are: file operations, character selection, display of the image in a window, printing, and debugging. For each area, we specified minimum requirements and identified optional features. Our spec provided plenty of leeway to the implementor to highlight features unique to their product. The complete spec is available electronically.
We supplied the data files and the specification for the file format; we also implemented DOS-based functions to read the data from the disk into an in-memory data structure, and other functions to display a letter on the screen using MoveTo() and LineTo() callback functions. Implementors were free to change the code to their liking, as long as functionality was preserved.
This was not a programming exam in the sense of trick questions or hidden problems to solve. We're only interested in looking at the final result, in understanding what it took to get there, and in discussing the technological issues surrounding frameworks, class libraries, and GUI toolkits. Also, we hoped to provide some good fun. In retrospect, it looks like we succeeded in that goal: More than one implementor told us that he welcomed the chance to do some uninterrupted programming.
--R.V.
_SIZING UP APPLICATION FRAMEWORKS AND CLASS LIBRARIES_ by Ray Valdes[LISTING ONE]
/******* BORLAND ******/ ....standard #includes... _CLASSDEF( TMainWindow ) _CLASSDEF( TMainApp ) //---------------------------- Constructor of Application's MainWindow. TMainWindow::TMainWindow( PTWindowsObject AParent,LPSTR AName,PTModule AModule) : TDialog( AParent, AName, AModule ) { fDataLoaded= FALSE; DrawData.cur_char = 'A'; DrawData.cur_inst = 0; DrawData.rgbBackGroundColor = GetSysColor( COLOR_WINDOW ); DrawData.rgbLineColor = GetSysColor( COLOR_WINDOWTEXT ); DrawData.nLineThickness = 1; DrawData.nScaleFactor = NORM_SCALE; GridWindow = new TGridWindow( this, ID_GRIDWINDOW , &DrawData ); ViewWindow = new TViewWindow( this, ID_VIEWWINDOW , &DrawData ); Printer = new TPrinter; ScaleScrollBar = new TScrollBar( this, ID_SCROLLSCALE ); ScaleText = new TStatic ( this, ID_SCALETEXT,-1); } //-------------------------------------- Destructor of App's MainWindow TMainWindow::~TMainWindow() { if ( Printer ) delete Printer; } //------------------------------------- return Main Window's Class Name LPSTR TMainWindow::GetClassName() { return "bordlg_MainDialog"; } //---- Use the Borland Custom Dialog Class and indicate the Application's Icon void TMainWindow::GetWindowClass( WNDCLASS& AWndClass ) { TDialog::GetWindowClass( AWndClass ); AWndClass.lpfnWndProc = BWCCDefDlgProc; AWndClass.hIcon = LoadIcon( GetApplication()->hInstance,"ApplicationIcon" ); } //----- Function called as a result of WM_INITDIALOG. Center Dialog on Screen. void TMainWindow::SetupWindow() { RECT rc; TDialog::SetupWindow(); GetWindowRect( HWindow, &rc ); OffsetRect( &rc, -rc.left, -rc.top ); MoveWindow( HWindow, (( GetSystemMetrics( SM_CXSCREEN ) - rc.right ) / 2 + 4 ) & ~7, ( GetSystemMetrics( SM_CYSCREEN ) - rc.bottom ) / 2, rc.right, rc.bottom, 0 ); ScaleScrollBar->SetRange( 1, 10 ); ScaleScrollBar->SetPosition( 5 ); EnableScaleScrollBar( FALSE ); ScaleText->SetText( "&Scale: 50%" ); SetFocus( GridWindow->HWindow ); } void TMainWindow::EnableScaleScrollBar( BOOL fFlag ) { EnableWindow( ScaleScrollBar->HWindow, fFlag ); EnableWindow( ScaleText->HWindow, fFlag ); } //---- Function responds to FILE|READ Menu Option... // Prompts user for FileName and Read Data... void TMainWindow::CMFileRead ( RTMessage ) { strcpy( lpszFileName , "*.dat" ); GetApplication()->ExecDialog( new TFileDialog( this , SD_FILEOPEN , lpszFileName )); SetWait(); if ( file_LoadHWXData( lpszFileName )) { fDataLoaded = TRUE; GridWindow->UpdateView(); ViewWindow->UpdateView(); } ReleaseWait(); } //-------- Responds to the Print Request... void TMainWindow::CMPrint(RTMessage) { PTPrintCharInst Printout = 0; if ( Printer ) { Printout = new TPrintCharInst( "Char Instance", DrawData.cur_char, DrawData.cur_inst, DrawData.nLineThickness, DrawData.nScaleFactor ); if ( Printout ) { Printout->SetBanding( TRUE ); Printer->Print(this, Printout); delete Printout; } } } //------- Allows user to Select/Configure Printer void TMainWindow::CMPrinterSetup(RTMessage) { if ( Printer ) Printer->Setup(this); } //--------- Request to Exit Menu Choice - Terminates App... void TMainWindow::CMExit( RTMessage ) { PostMessage( HWindow, WM_SYSCOMMAND, SC_CLOSE, 0 ); } //--------------------------------------Bring up AboutBox void TMainWindow::CMAbout( RTMessage ) { GetApplication()->ExecDialog( new TDialog( this , "ABOUTBOX" )); } void TMainWindow::UpdateSubViews( void ) { GridWindow->UpdateView(); ViewWindow->UpdateView(); } //----- Allows user to select Foreground/Line Color. void TMainWindow::IDForColor( RTMessage ) { PTColorDialog ColorDialog = new TColorDialog( this , DrawData.rgbLineColor ); if ( ColorDialog ) { delete ColorDialog; UpdateSubViews(); } } //--------------------------------------user selects Background Color void TMainWindow::IDBackColor( RTMessage ) { PTColorDialog ColorDialog = new TColorDialog( this , DrawData.rgbBackGroundColor , TRUE ); if ( ColorDialog ) { delete ColorDialog; UpdateSubViews(); } } //-------------------------------------- Responds to Line Thickness Selection void TMainWindow::IDLineRad1( RTMessage ) { DrawData.nLineThickness = 1; UpdateSubViews(); } //---------------------------------- user can specify scale for GridDisplay void TMainWindow::IDScrollScale( RTMessage ) { int nPos = ScaleScrollBar->GetPosition(); if ( nPos != DrawData.nScaleFactor ) { char buff[80]; wsprintf( buff, "&Scale: %d%%", nPos*10 ); DrawData.nScaleFactor = MAX_SCALE + MIN_SCALE - nPos; ScaleText->SetText( buff ); GridWindow->UpdateView(); } } //---- Sent by GridWindow to MainWindow which then informs ViewWindow of change void TMainWindow::SetCurChar( int ch, int inst ) { if ( DrawData.cur_char != ch || DrawData.cur_inst != inst ) { DrawData.cur_char = ch; DrawData.cur_inst = inst; ViewWindow->UpdateView(); // Allows View to Refresh } } //---------------------------------- Constructor of Main Application Class TMainApp::TMainApp( LPSTR AName, HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmd, int nCmdShow ) : TApplication( AName, hInstance, hPrevInstance, lpCmd, nCmdShow ) { } //--------------------------------- Specifying the Application's Main Window void TMainApp::InitMainWindow() { BWCCGetVersion(); // Force Implicit Loading of BWCC.DLL ! MainWindow = new TMainWindow( NULL, "MainDialog" ); } //---------------------------------- Create Application Class and run ! int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmd, int nCmdShow ) { TMainApp MyApp( "HWX Browser", hInstance, hPrevInstance, lpCmd, nCmdShow ); MyApp.Run(); return( MyApp.Status ); }[LISTING TWO]
/******** INMARK *******/ #include "zapp.hpp" #include "ddj.hpp" typedef int (zEvH::*KeyProc)(zKeyEvt*); Alphabet *alpha=0; char *types[]={ "HWX Files (*.dat)","*.dat", "All Files (*.*)","*.*", 0,0 }; void Browser::removeViews() { if (grid!=0) { grid->sizer()->remove(); has()->remove(grid); delete grid; grid=0; } if (curVis!=0) { curVis->sizer()->remove(); has()->remove(curVis); } if (ilet!=0) { ilet->sizer()->parent()->remove(); delete ilet->sizer()->parent(); has()->remove(ilet); delete ilet; ilet=0; } } void Browser::changeLetInInstView() { removeViews(); curInst = 0; ilet=new VisLetter(this,center,new zPercentSizer(left,100,50,sizer()),curLet,1); has()->append(ilet); grid=new GridCompound(this,sizer()); select(grid->addLetter(alpha,curVis->letter()->ch())); sizer()->update(); canvas()->setDirty(); updateStats(selVis); } void Browser::changeInstanceInAlphaView(int spos) { removeViews(); curInst = spos; grid=new GridCompound(this,sizer()); select(grid->addAlphabet(alpha,curInst)); updateStats(selVis); sizer()->update(); canvas()->setDirty(); } void Browser::changeLetterFromScrollPos(int spos) { int cnt=0,let=0; while (let <HICHAR && cnt <=spos) if (alpha->instance(let++,curInst)!=0) cnt++; changeChar(let-1,curInst); } void Browser::changeLetFromInstScr(int spos) { int cnt=0,let=0; while (let <HICHAR && cnt <=spos) if (alpha->instance(let++,curInst)!=0) cnt++; curLet = alpha->instance(let-1,curInst); curVis->letter(curLet); } InfoPane::InfoPane(zWindow *w, zSizer *siz):VisualPane(w,siz) { VisPanel *pan; pan=new VisPanel(this,new zPercentSizer(left,100.0,33.3,siz)); add(pan); zSizer *x2; x2=new TextFrameSizer(middle,zDimension(0,0),sizer(),pan); totalc=new InfoText(this,x2,"Total Chars",pan); fsize=new InfoText(this,x2,"Size",pan); totalseg=new InfoText(this,x2,"Total Segments",pan); pan=new VisPanel(this,new zPercentSizer(left,100.0,33.3,siz)); add(pan); x2=new TextFrameSizer(middle,zDimension(0,0),sizer(),pan); wchar=new InfoText(this,x2,"Char",pan); cinst=new InfoText(this,x2,"Instance",pan); scalef=new InfoText(this,x2,"Scaling Factor",pan); pan=new VisPanel(this,new zPercentSizer(left,100,33.3,siz)); add(pan); x2=new TextFrameSizer(middle,zDimension(0,0),sizer(),pan); nstrok=new InfoText(this,x2,"Strokes",pan); npoints=new InfoText(this,x2,"Points",pan); csize=new InfoText(this,x2,"Size",pan); backgroundColor(zColor(192,192,192)); } Browser::Browser(zWindow *w,zSizer *siz, InfoPane *inp):VisualPane(w,siz,zSCROLLV|zSCROLLH) { ilet=selVis=0; curLet=0; curInst=curWidth=0; curView=IDM_SVIEW; ip=inp; curVis=new VisLetter(this,center,sizer(),0,1); add(curVis); has()->alwaysHit(); } BOOL Browser::getPreferredSize(zDimension& d) { d.width() = 500; d.height() = alpha->maxSize().height()*grid->vertKids(); return 1; } void Browser::updateStats(VisLetter *vl) { char buf[50]; wsprintf(buf," %d - %c ",(int)vl->letter()->ch(), (char)vl->letter()->ch()); ip->wchar->setVal(buf); wsprintf(buf," %d of %d ",vl->letter()->instance()+1, alpha->letterInstances(vl->letter()->ch())); ip->cinst->setVal(buf); wsprintf(buf," %d x %d ", vl->maxSize().width(),vl->maxSize().height()); ip->csize->setVal(buf); ip->nstrok->setVal(vl->letter()->strokes()); int tot=vl->letter()->strokes(); for (int i=0;i<vl->letter()->strokes();i++) tot+=vl->letter()->path(i).length(); ip->npoints->setVal(tot); ip->scalef->setVal(vl->scaling()); } int Browser::ch(zKeyEvt* e) { if (curView == IDM_SVIEW || curView == IDM_IVIEW) { Letter *test=0; if (alpha!=0) test=alpha->instance(e->ch(),0); if (test!=0 && test!=curLet) { if (curView == IDM_SVIEW) changeChar(e->ch(),0); else { curLet = test; curVis->letter(curLet); int mnum = 0; for (int cnt = 0; cnt < curVis->letter()->ch();cnt++) if (alpha->instance(cnt,curInst)!=0) mnum++; scrollBarVert()->pos(mnum); scrollBarVert()->oldpos(mnum); changeLetInInstView(); } } } return 0; } int Browser::printView(zCommandEvt* e) { if (alpha==0) { zMessage(app->rootWindow(),"No data file loaded","Error"); return 1; } zPrinterDisplay *pr=new zPrinterDisplay; if (!pr->isValid()) { zMessage mess(this,"No Printer drivers installed","Unable to print"); return 1; } if (pr->printerSetup()) { zDefPrJobDlg *prDlg=new zDefPrJobDlg(parent(), zResId(PRINT)); zPrintJob *pj = new zPrintJob(this, pr, prDlg); pj->setJobName("HWX Print Job"); pj->go(); } return 1; } int Browser::print(zPrinterDisplay *pd, zRect *re) { zRect r; pushDisplay(pd); canvas()->lock(); // Re-Lay out the display based on the printers display; canvas()->getVisible(r); zRect save(*sizer()); sizer()->update(&r); draw(0); // Restore the old display layout. canvas()->unlock(); sizer()->update(&save); popDisplay(); return 0; } int Browser::changeView(zCommandEvt* e) { int which,cnt=0; if (e==0) { curView=0; which=IDM_SVIEW; } else which=e->cmd(); if (which==curView || alpha==0) return 1; selVis=0; curInst = 0; app->rootWindow()->menu()->checkItem(curView,FALSE); app->rootWindow()->menu()->checkItem(which,TRUE); if (scroller()) delete scroller(); removeViews(); switch (which) { case IDM_SVIEW: sizer()->append(curVis->sizer()); has()->append(curVis); scroller(new BroScroller(this)); scrollBarVert()->limits(zRange(0,alpha->totalChars()-1)); for (cnt = 0; alpha->instance(cnt,curInst)==0;cnt++); changeChar(cnt,curInst,0); scrollBarVert()->oldpos(scrollBarVert()->pos()); scrollBarHoriz()->pos(curInst); scrollBarHoriz()->oldpos(curInst); ip->totalc->setVal(alpha->totalChars()); ip->fsize->setVal(alpha->fileSize()); ip->totalseg->setVal(alpha->totalSegments()); break; case IDM_IVIEW:{ ilet=new VisLetter(this,center,new zPercentSizer(left,100,50,sizer()),curLet,1); has()->append(ilet); grid=new GridCompound(this,sizer()); select(grid->addLetter(alpha,curLet->ch())); updateStats(selVis); scroller(new InstanceScroller(this)); scrollBarVert()->limits(zRange(0,alpha->totalChars()-1)); int mnum=0; for (cnt = 0; cnt < curVis->letter()->ch();cnt++) if (alpha->instance(cnt,curInst)!=0) mnum++; scrollBarVert()->pos(mnum); scrollBarVert()->oldpos(mnum); break;} case IDM_AVIEW: grid=new GridCompound(this,sizer()); select(grid->addAlphabet(alpha,curInst)); updateStats(selVis); scroller(new AlphabetScroller(this)); scroller()->respondToSize(); } sizer()->update(); canvas()->setDirty(); curView=which; return 1; } void Browser::changeChar(int wh,int ins,int updateNow) { curInst=ins; curLet=alpha->instance(wh,ins); curVis->letter(curLet); updateStats(curVis); scrollBarHoriz()->limits(zRange(0, (alpha->letterInstances(curVis->letter()->ch())-1))); int mnum=0; for (int cnt=0; cnt < curVis->letter()->ch();cnt++) if (alpha->instance(cnt,curInst)!=0) mnum++; scrollBarVert()->pos(mnum); if (updateNow) { canvas()->setDirty(); UpdateWindow(*this); } } void Browser::select(VisLetter *vl) { if (selVis!=vl) { if (selVis!=0) selVis->select(FALSE); selVis=vl; selVis->select(TRUE); if (ilet!=0) { ilet->letter(selVis->letter()); updateStats(ilet); } else updateStats(selVis); } } int Browser::changeLineWidth(zCommandEvt* e) { AskNumForm *ask; ask=new AskNumForm(new zFormDlg(((HwxApp *)parent()), zResId("ASKNUMFORM")),curWidth); if (ask->completed() && ask->width()!=curWidth) { VisLetter::setPen(new zPen(def[IDM_LINEC-IDM_BACK],Solid, curWidth=ask->width())); canvas()->setDirty(); } delete ask; return 1; } int Browser::changeColor(zCommandEvt* e) { zColorSelForm *tmp=new zColorSelForm(this,def[e->cmd()-IDM_BACK]); if (tmp->completed()) { def[e->cmd()-IDM_BACK]=tmp->color(); switch (e->cmd()) { case IDM_BACK: backgroundColor(tmp->color()); break; case IDM_LINEC: VisLetter::setPen(new zPen(tmp->color(),Solid,curWidth)); break; case IDM_HILIGHTC: VisLetter::setHighlightColor(tmp->color()); break; case IDM_LINEB: VisLetter::setBrush(new zBrush(tmp->color())); } canvas()->setDirty(); } return 1; } HwxToolBar::HwxToolBar(zWindow *w,zSizer *s,DWORD d):ToolBar(w,s,d) { backgroundColor(zColor(LTGRAY)); frame->append(new VisBmpButton(this,top,sizer(),IDM_OPEN,LINEI,LINEI+1)); frame->append(new VisBmpButton(this,top,sizer(),IDM_PRINT,PRI,PRI+1)); frame->append(new VisBmpButton(this,top,sizer(),IDM_SVIEW,SVI,SVI+1)); frame->append(new VisBmpButton(this,top,sizer(),IDM_IVIEW,IVI,IVI+1)); frame->append(new VisBmpButton(this,top,sizer(),IDM_AVIEW,AVI,AVI+1)); } HwxApp::HwxApp(zWindow *w,zSizer *siz,DWORD d,const char *title) : zAppFrame(w,siz,d,title) { menu(new zMenu(this,zResId("TOPMENU"))); menu()->setCommand(this,(CommandProc)&HwxApp::fOpen,IDM_OPEN); setIcon(new zIcon(zResId("DRAWICON"))); sline=new StatusLine(this,new zGravSizer(bottom,0)); sline->show(); sline->setupHilite(); toolbar=new HwxToolBar(this,new zGravSizer(left,32),WS_BORDER); toolbar->show(); ip = new InfoPane(this, new zGravSizer(top,zPrPoint(0,590))); ip->show(); image=new Browser(this,new zGravSizer(middle),ip); menu()->setCommand(image, (CommandProc)&Browser::changeColor,IDM_BACK,IDM_LINEB); menu()->setCommand(image, (CommandProc)&Browser::changeLineWidth,IDM_LINEW); menu()->setCommand(image, (CommandProc)&Browser::changeView,IDM_SVIEW,IDM_AVIEW); menu()->setCommand(image,(CommandProc)&Browser::printView,IDM_PRINT); image->show(); } HwxApp::~HwxApp() { delete sline; delete toolbar; delete image; } int HwxApp::fOpen(zCommandEvt*e) { zFileOpenForm *tmp=new zFileOpenForm(this,"Select File",0,types); if (tmp->completed()) { sline->message()->printf("Loading File..."); setCursor(zCursor(Hourglass)); char buf[300]; wsprintf(buf,"zApp HWX Browser - %s",tmp->name()); caption(buf); alpha=new Alphabet(); alpha->readFile(tmp->name()); if (alpha->isValid()) { image->addVertScrollBar(); image->addHorzScrollBar(); image->changeView(); sline->message()->printf("File Loaded"); } else alpha=0; setCursor(zCursor(Arrow)); } return 1; } int HwxApp::focus(zFocusEvt* e) { if (e->gainFocus() && image!=0) image->setFocus(); return 1; } int HwxApp::command(zCommandEvt*e) { switch (e->cmd()) { case IDM_EXIT: app->quit(); break; case IDM_HELP: WinHelp(*this,"ddj.hlp",HELP_INDEX,0); break; case IDM_ABOUT:{ AboutBox *ab = new AboutBox(this,new zSizer,"About Box"); delete ab;} } return 1; } void zApp::main() { HwxApp* p=new HwxApp(0,new zSizer,zSTDFRAME,"zApp HWX Browser"); p->show(); go(); delete p; }[LISTING THREE]
/***** ISLAND SYSTEMS IMPLEMENTATION USING OBJECT-MENU*****/ #include "omExt.h" #include "h_config.h" #include "h_stddef.h" #include "h_list.h" #include "h_grafic.h" #include "h_om.h" #include "omExt.h" #include "h_config.h" #include "h_stddef.h" #include "h_list.h" #include "h_grafic.h" #include "h_om.h" extern void LoadHWXData(LPSTR name); extern void omDisplayInstance(lpRect R, int char_code, int instance_num, int shift, int boxcolor, int drawcolor); extern void RegisterCallback( HDISPLAY display_entity, void (*pf_move_to)(HDISPLAY,INT16,INT16), void (*pf_line_to)(HDISPLAY,INT16,INT16)); extern void myMoveTo(HDISPLAY display_entity,INT16 h,INT16 v); extern void myLineTo(HDISPLAY display_entity,INT16 h,INT16 v); //----- globals const char titlePredicate[] = "Sample File: "; omDispHelpBlock *dispHelp; ...subordinate routines deleted... //-----------------MAIN FUNCTION------------------ int omRunIt() { //------ initialize help and icon libraries: hw.dlb, hw.ilb omLibMgr = new omLibMgrType("hw"); if (omLibMgr==NULL) {omBeep(); return 1;} // enough memory? if (!omLibMgr) {omBeep(); return omLibMgr->lastErr;} // init ok? RegisterCallback(0, myMoveTo, myLineTo); // DDJ callbacks dispHelp = new omDispHelpBlock( 100, 4*omFontHeight, 0,0); dispHelp->usedNew = TRUE; dispHelp->pageBkColor = LIGHTGRAY; dispHelp->theDress = RIDGE; //------- FILE MENU: allocate and assign items to the "File" pulldown menu hwFileMenu *fileMenu = new hwFileMenu(4); if (fileMenu==NULL) return 1; fileMenu->usedNew = omTRUE; *fileMenu + "~Open" + "~About" + "_" + "E~xit"; fileMenu->assignId( ID_FILEMENU ); omIDTABLE.add(ID_FILEMENU, fileMenu); fileMenu->setEventHelp( "file" ); // set help prefix fileMenu->setDisplayHelp( dispHelp ); // attach help //------- EDIT MENU: allocate and assign items to the "Edit" pulldown menu omVertMenu *editMenu = new omVertMenu( 5 ); if (editMenu==NULL) return 1; editMenu->usedNew = omTRUE; *editMenu + "~Cut" + DISABLE + "~Copy" + "?copy" //+ DISABLE + "~Paste" + DISABLE + "_" + "Undo" + DISABLE + IS_TEAROFF; editMenu->setEventHelp( "edit" ); // set help prefix editMenu->setDisplayHelp( dispHelp ); // attach help //------- OPTION MENU: allocate and assign items to the "Edit" pulldown menu hwOptMenu *optMenu = new hwOptMenu( 2 ); if (optMenu==NULL) return 1; optMenu->usedNew = omTRUE; *optMenu + "~Open new browser !!" + "?open" + "~Single letter !!" + "?single"; optMenu->setEventHelp( "optmenu" ); // set help prefix optMenu->setDisplayHelp( dispHelp ); // attach help //------- TOP MENU: allocate and assign items to Horizontal menu bar omHorizMenu *topMenu = new omHorizMenu( 6 ); if (topMenu==NULL) return 1; topMenu->usedNew = omTRUE; *topMenu + "~File" + "?file" + *fileMenu + "~Edit" + "?edit" + *editMenu + "~Browse" + "?browse" + *optMenu + "~Printer| setup" + "?printcfg" + psetup + "dela~yed|help" + omIconFcnRadioDiam + changeHelpTimeOut + "?delayedhelp" + IS_RADIO + "~immediate|help" + omIconFcnRadioDiam + changeHelpTimeOut + "?immedhelp"; if ( omPrefs.helpTimeout>0 ) topMenu->autoRadioToggle(4); // delayed help else topMenu->autoRadioToggle(5); // immed help topMenu->setEventHelp( "topmenu" ); // set help prefix topMenu->setDisplayHelp( dispHelp ); // attach help topMenu->assignId(ID_TOPMENU); //------------ WINDOW: define the window omBkWindow *w = new omBkWindow(); w->bkColor = CYAN; if (w==NULL) return 1; w->usedNew = omTRUE; w->setTitle("Island Systems Handwriting Utility" " Rev 1.1|Dr. Dobb's Journal, October 1992"); w->assignId(ID_BKWINDOW); w->setDrawModeBk( omCOLOR ); *w + *topMenu + omTL + *dispHelp + omBL; w->makeTask(); //------------- RUN: put the window on the event Queue and run. *omMEQ + *w; omMEQ->run(); return 0; }[LISTING FOUR]
/****** LIANT ******/ #include "about.h" #include "brush.h" #include "bttnrbbn.h" #include "button.h" #include "charview.h" #include "file.h" #include "fileslct.h" #include "font.h" #include "hwxappvw.h" #include "notifier.h" #include "objarray.h" #include "pen.h" #include "pointrry.h" #include "popupmen.h" #include "port.h" #include "printer.h" #include "printdlg.h" #include "printab.h" #include "statusbr.h" #include <stdio.h> // for sprintf() #define BR_HT 40 // height of the Button Bar window #define STAT_HT 24 // height of the Status Bar window defineClass(HwxAppView, VAppView) /* menu definition (could also come from a resource) */ char *file[] = { "&Open...", "", "&Print...", "", "&About...", "", "E&xit", 0 }; method fmthds[] = { methodOf(HwxAppView, openFile),NIL_METHOD, methodOf(HwxAppView, print),NIL_METHOD, methodOf(HwxAppView, about),NIL_METHOD, methodOf(HwxAppView, closeApp),NIL_METHOD }; char *display[] = { "&Style...", "", "&Instance", "&Alphabet", 0 }; method dmthds[] = { methodOf(HwxAppView, setDisplayStyle),NIL_METHOD, methodOf(HwxAppView, showByInstance), methodOf(HwxAppView, showByAlphabet),NIL_METHOD }; /* icon bar definition */ char *bttns[] = { "FILEBOX","PRINTER","PALETTE","ALPHA",0}; method bMthds[] = { methodOf(HwxAppView, btnOpen), methodOf(HwxAppView, btnPrint), methodOf(HwxAppView, btnStyle), methodOf(HwxAppView, btnAlpha), NIL_METHOD }; HwxAppView::HwxAppView() { /* set title and background brush for main window */ setTitle("Liant HWX: (no file loaded)"); setBackground(new VBrush(BLUE)); /* create the icon bar */ bttnRbbn = new ButtonRibbon(VFrame(0, 0, 1.0F, BR_HT), this, this, bttns, bMthds); /* create a status bar */ int x, y, w, h; getArea(&x, &y, &w, &h); statBar = new StatusBar(VFrame(0, h - STAT_HT, 1.0F, STAT_HT), this); statBar->putText("HWX Browser"); statBar->setFont(new VFont("TMS RMN", 10)); /* create the pull down menu (using arrays defined above) */ VPopupMenu *pm = new VPopupMenu("&File"); addPopup(pm); pm->addItems(file, fmthds, this); displayMenu = new VPopupMenu("&Display"); addPopup(displayMenu); displayMenu->addItems(display, dmthds, this); displayMenu->enableAll(FALSE); displayMenu->checkAt(TRUE, 2); /* create the single character view sub-window */ charView = new CharacterView(VFrame(10, 50, 100, 100), this); /* create the multi-instance view sub-window */ multiView = new MultiCellView(VFrame(130, 50, 7 * 60, 200), this); multiView->setDims(6, 13, 30, 30); multiView->uponClick(this, methodOf(HwxAppView, multiSelect)); /* create the character selection sub-window */ selectView = new AlphaCellView(VFrame(10, 300, 17 * 32 + 6, 17 * 3 + 6), this); selectView->setDims(3, 32, 17, 17); selectView->uponClick(this, methodOf(HwxAppView, charSelect)); /* create pen for drawing single character instance */ charPen = new VPen(); charData = 0; byInstance = TRUE; setCharacter(48, 0); } HwxAppView::~HwxAppView() { /* destroy status bar font */ VFont *f = statBar->getFont(); if (f) { f->free(); } /* destroy background brush */ if (getBackground()) { delete getBackground(); } /* destroy character pen */ if (charPen) { delete charPen; } /* destroy associated character data */ if (charData) { delete charData; } } boolean HwxAppView::free() { delete this; return(TRUE); } boolean HwxAppView::paint() { VPort port(this); int x, y, w, h; getArea(&x, &y, &w, &h); port.open(); /* draw a separator above the staus bar */ port.moveTo(0, h - STAT_HT - 1); port.lineTo(w, h - STAT_HT - 1); port.close(); return(TRUE); } boolean HwxAppView::resized(int w,int h) // called when window changes size. { // resize the ButtonRibbon bttnRbbn->resize(w, BR_HT); // move the StatusBar up to bottom of MDI client statBar->move(0, h - STAT_HT, w, STAT_HT); return(TRUE); } boolean HwxAppView::openFile(VMenuItem *m) // Load a new data file { VString *temp; VString filter("*.dat"); char str[100]; temp = VFileSelect::dialog(this, "Select Data File", NIL, &filter); if (temp) { notifier->beginWait(); statBar->putText("Loading File..."); VFile f(temp->gets()); if (f.open(ReadOnly)) { if (charData != 0) { delete charData; charData = 0; } charData = new CharacterData(f); f.close(); /* update window title */ sprintf(str, "Liant HWX: (%s)", temp->gets()); setTitle(str); /* enable display menu */ displayMenu->enableAll(TRUE); } notifier->endWait(); setCharacter(48, 0); } return(TRUE); } boolean HwxAppView::closeApp(VMenuItem *m) // Close the application { return(VAppView::close()); } boolean HwxAppView::print(VMenuItem *m) // Print the current character. { VPrinter *printer = 0; /* get a printer object via the printer dialog */ boolean ret = VPrintDialog::print(NIL, &printer, this); if (!ret) { statBar->putText("Printer not ready."); } else { /* display the print abort dialog */ VPrintAbort *pab = new VPrintAbort("HWX", "Printing character.", printer, this); /* open a port on the printer */ VPort port(printer); VRectangle rect(CornerDim, 50, 50, 300, 300); port.open(); /* draw the character on the printer */ charView->drawCharacter(&port, &rect, 5.0); // printer->newPage(); port.close(); pab->free(); printer->free(); } return(TRUE); } boolean HwxAppView::about(VMenuItem *m) // Display the About box { VAbout::dialog("Liant HWX Browser\n\n) 1992 by Liant Software Corp." "\nWritten by Joe DeSantis\n" "\nDeveloped with C++/Views 2.0", this); return(TRUE); } boolean HwxAppView::setDisplayStyle(VMenuItem *m) /* Put up a dialog, and set the width and color of the character display. */ { int width; rgbColor color; width = charPen->width(); color = charPen->color(); DisplayDialog::dialog(this, &width, &color); charPen->width(width); charPen->color(color); charView->update(); return(TRUE); } boolean HwxAppView::showByAlphabet(VMenuItem *m) { displayMenu->checkAt(FALSE, 2); displayMenu->checkAt(TRUE, 3); setByInstance(FALSE); return(TRUE); } boolean HwxAppView::showByInstance(VMenuItem *m) { displayMenu->checkAt(TRUE, 2); displayMenu->checkAt(FALSE, 3); setByInstance(TRUE); return(TRUE); } void HwxAppView::setCharacter(int cno, int ino) { // Set the current character/instance displayed. char str[100]; currChar = cno; currInst = ino; int icount = (charData) ? charData->getInstanceCount(currChar) : 0; /* update status bar */ sprintf(str, "Character %d, Instance %d out of %d", currChar, currInst, icount); statBar->putText(str); /* update views */ charView->setCharData(charData, currChar, currInst); multiView->setCharData(charData, currChar, currInst); selectView->setCharData(currChar, currInst); } void HwxAppView::charSelect(long row, long col) { // AlphaCellView box clicked in cell row, col if (byInstance) { /* view a new character */ setCharacter((int) ((row + 1) * 32 + col), currInst); } else { /* view a new instance */ setCharacter(currChar, (int) (row * 16 + col)); } } void HwxAppView::multiSelect(long row, long col) { // MultiCellView box clicked in cell row, col if (byInstance) { /* view a new instance */ setCharacter(currChar, (int) (row * 13 + col)); } else { /* view a new character */ setCharacter((int) (row * 13 + col + 48), currInst); } } void HwxAppView::setByInstance(boolean tf) /* Specifies whether to display all instances of a single character, or an alphabet of a specific instance. */ { if (byInstance != tf) { byInstance = tf; if (tf) { selectView->setDims(3, 32, 17, 17); } else { selectView->setDims(3, 16, 34, 17); } multiView->setByInstance(tf); selectView->setByInstance(tf); setCharacter(48, 0); selectView->update(); } } boolean HwxAppView::btnOpen(VButton *b) // Callback for file icon { openFile(NIL); return(TRUE); } boolean HwxAppView::btnPrint(VButton *b) // Callback for print icon { print(NIL); return(TRUE); } boolean HwxAppView::btnStyle(VButton *b) // Callback for style icon { setDisplayStyle(NIL); return(TRUE); } boolean HwxAppView::btnAlpha(VButton *b)// Callback for alpha/instance icon { if (byInstance) { showByAlphabet(NIL); } else { showByInstance(NIL); } return(TRUE); }[LISTING FIVE]
/****** MICROSOFT ******/ //------ CHwbDlg: The main user interface to this application is a dialog class CHwbDlg : public CModalDialog { // Constructors public: CHwbDlg(); ~CHwbDlg(); // Attributes // global state of browser COLORREF m_colorCur; int m_nLineWidth; // current state (based on selection in .DAT file) BOOL m_bLoaded; // .DAT file loaded char m_chCur; // current character int m_nInstanceCount; // number of instance of this character int m_iInstanceCur; // current instance of that character // Operations void RenderInstance(CDC* pDC, char ch, int iInstance, CRect rect, int inflate, BOOL bAttrib); // Implementation CImageBox m_charBox; // all possible characters protected: // brushes for drawing CBrush m_brBack, m_brGray; CFont m_statusFont; // special controls CBitmapButton m_buttons[NUM_BITMAPBUTTONS]; CImageBox m_imageBox; // image selection CStatic m_display; // display output control CScrollBar m_scrollBar; CRect GetOutputRect(); // message handlers virtual BOOL OnInitDialog(); virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam); afx_msg void OnPaint(); afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar*); afx_msg void OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hSysMenu); // command handlers afx_msg void OnOpen(); afx_msg void OnCopy(); afx_msg void OnPrint(); afx_msg void OnAbout(); afx_msg void OnPrevChar(); afx_msg void OnNextChar(); afx_msg void OnLineColor(); // control notification handlers afx_msg void OnInstanceSelect(); afx_msg void OnCharSelect(); #ifdef _DEBUG afx_msg void OnShowStrokes(); #endif DECLARE_MESSAGE_MAP() }; // Message map defines messages handled by our main window BEGIN_MESSAGE_MAP(CHwbDlg, CModalDialog) ON_WM_PAINT() ON_WM_HSCROLL() ON_WM_MENUSELECT() ON_COMMAND(IDM_OPEN, OnOpen) ON_COMMAND(IDM_COPY, OnCopy) ON_COMMAND(IDM_PRINT, OnPrint) ON_COMMAND(IDM_ABOUT, OnAbout) ON_COMMAND(IDC_PREV, OnPrevChar) ON_COMMAND(IDC_NEXT, OnNextChar) ON_COMMAND(IDM_LINE_COLOR, OnLineColor) #ifdef _DEBUG // debug commands ON_COMMAND(IDM_DEBUG_SHOW_STROKES, OnShowStrokes) #endif //_DEBUG ON_CBN_SELCHANGE(IDC_INSTANCE_BOX, OnInstanceSelect) ON_CBN_SELCHANGE(IDC_ALLCHARS_BOX, OnCharSelect) END_MESSAGE_MAP() //----------------------------------------------------------------- CHwbDlg::CHwbDlg() : CModalDialog("MAINDIALOG"), m_brBack(::GetSysColor(COLOR_WINDOW)), m_brGray(RGB(128, 128, 128)) { m_bLoaded = FALSE; m_colorCur = RGB(0, 0, 0); // black to start with m_nLineWidth = 1; } //----------------------------------------------------------------- CHwbDlg::~CHwbDlg() { if (m_bLoaded) ::UnloadHWXData(); // free old data file } //----------------------------------------------------------------- BOOL CHwbDlg::OnInitDialog() { // initialize bitmap buttons for (int i = 0; i < NUM_BITMAPBUTTONS; i++) VERIFY(m_buttons[i].AutoLoad(IDC_BITMAPBUTTON_MIN + i, this)); // wire in the dialog controls VERIFY(m_imageBox.SubclassDlgItem(IDC_INSTANCE_BOX, this)); m_imageBox.m_pParent = this; VERIFY(m_charBox.SubclassDlgItem(IDC_ALLCHARS_BOX, this)); m_charBox.m_pParent = this; VERIFY(m_scrollBar.SubclassDlgItem(IDC_SCROLLBAR, this)); // select a lighter font for the status bar LOGFONT logfont; GetFont()->GetObject(sizeof(logfont), &logfont); logfont.lfWeight = FW_NORMAL; VERIFY(m_statusFont.CreateFontIndirect(&logfont)); GetDlgItem(IDC_STATUS)->SetFont(&m_statusFont); OnOpen(); // require an open data file if (!m_bLoaded) EndDialog(IDCANCEL); // abort return TRUE; } //----------------------------------------------------------------- void CHwbDlg::OnOpen() { CFileDialog dlg(TRUE, "dat", NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, "Data Files (*.dat)|*.dat|All Files (*.*)|*.*||"); // constructor for standard file open dialog if (dlg.DoModal() != IDOK) return; // stay with old data file HCURSOR hCurs = ::SetCursor(::LoadCursor(NULL, IDC_WAIT)); // set the wait cursor if (m_bLoaded) ::UnloadHWXData(); // free old data file // open new data file ::LoadHWXData(dlg.GetPathName()); m_bLoaded = TRUE; // fill listbox with all valid characters using exported Dr. Dobbs APIs m_charBox.ResetContent(); for (char ch = 0; ch <= 126; ch++) // simple ASCII range { if (::GetInstanceCount(ch) == 0) continue; // skip characters with no instances int nIndex = m_charBox.AddString(MAKEINTRESOURCE(ch)); if (ch == 'A') m_charBox.SetCurSel(nIndex); // initial selection } OnCharSelect(); // update instances ::SetCursor(hCurs); // change cursor back to standard } //----------------------------------------------------------------- void CHwbDlg::OnCopy() { // copy current image to clipboard if (!OpenClipboard()) return; CMetaFileDC dc; if (dc.Create()) { RenderInstance(&dc, m_chCur, m_iInstanceCur, CRect(0,0,0,0), 0, TRUE); HGLOBAL hData = ::GlobalAlloc(GPTR, sizeof(METAFILEPICT)); if (hData != NULL) { LPMETAFILEPICT lpMFP = (LPMETAFILEPICT)::GlobalLock(hData); lpMFP->mm = MM_ANISOTROPIC; lpMFP->hMF = dc.Close(); VERIFY(EmptyClipboard()); ::GlobalUnlock(hData); SetClipboardData(CF_METAFILEPICT, hData); } VERIFY(CloseClipboard()); } } //----------------------------------------------------------------- void CHwbDlg::OnPrint() { // print the current image CPrintDialog dlg(FALSE, PD_NOPAGENUMS | PD_NOSELECTION); if (dlg.DoModal() != IDOK) return; HCURSOR hCurs = ::SetCursor(::LoadCursor(NULL, IDC_WAIT)); // set the wait cursor CDC dc; dc.Attach(dlg.GetPrinterDC()); CRect rect; dc.GetClipBox(rect); dc.StartDoc("HandWriting"); dc.StartPage(); RenderInstance(&dc, m_chCur, m_iInstanceCur, rect, 0, FALSE); dc.EndPage(); dc.EndDoc(); ::SetCursor(hCurs); // return to standard cursor } void CHwbDlg::OnAbout() { CModalDialog dlg("ABOUTDIALOG"); dlg.DoModal(); } void CHwbDlg::OnNextChar() { m_charBox.SetCurSel(m_charBox.GetCurSel() + 1); OnCharSelect(); } void CHwbDlg::OnPrevChar() { m_charBox.SetCurSel(m_charBox.GetCurSel() - 1); OnCharSelect(); } void CHwbDlg::OnCharSelect() { ASSERT(m_bLoaded); if (m_charBox.GetCurSel() == -1) m_charBox.SetCurSel(0); // select first one m_chCur = (char)m_charBox.GetItemData(m_charBox.GetCurSel()); // update the character indicator and box SetDlgItemText(IDC_CHARACTER, CString(m_chCur)); m_nInstanceCount = ::GetInstanceCount(m_chCur); m_scrollBar.SetScrollRange(0, m_nInstanceCount-1); // update the list of possible instances m_imageBox.ResetContent(); for (int i = 0; i < m_nInstanceCount; i++) m_imageBox.InsertString(i, NULL); // data not used m_imageBox.SetCurSel(m_iInstanceCur = 0); // start at first one OnInstanceSelect(); // initial update } void CHwbDlg::OnInstanceSelect() { int iSel = m_imageBox.GetCurSel(); if (iSel != -1) { ASSERT(iSel >= 0 && iSel < m_nInstanceCount); m_iInstanceCur = iSel; } // update output ASSERT(m_bLoaded); ASSERT(m_iInstanceCur >= 0 && m_iInstanceCur < m_nInstanceCount); m_scrollBar.SetScrollPos(m_iInstanceCur); InvalidateRect(GetOutputRect(), FALSE); // will redraw at paint time } //-------- Encapsulated drawing code CRect CHwbDlg::GetOutputRect() { CRect rect; GetDlgItem(IDC_OUTPUT)->GetWindowRect(rect); ScreenToClient(rect); rect.InflateRect(-2, -2); // leave border alone return rect; } //----- functions to map Dr Dobbs API calls to CDC member functions static void _MoveTo(CDC* pDC, int x, int y) { pDC->MoveTo(x, y); } static void _LineTo(CDC* pDC, int x, int y) { pDC->LineTo(x, y); } //------- Render a character in the DC passed in. All drawing takes place here void CHwbDlg::RenderInstance(CDC* pDC, char ch, int iInstance, CRect rect, int inflate, BOOL bAttrib) { // fill in background if (inflate) pDC->FillRect(rect, &m_brBack); if (iInstance < 0) return; // nothing more to do rect.InflateRect(inflate, inflate); if (inflate != 0 && bAttrib) pDC->FrameRect(rect, &m_brGray); pDC->SaveDC(); // draw scaled to fit with optional attributes CPen pen(PS_INSIDEFRAME, m_nLineWidth*2+1, m_colorCur); if (bAttrib) pDC->SelectObject(&pen); pDC->SetMapMode(MM_ANISOTROPIC); if (!rect.IsRectEmpty()) { pDC->SetViewportExt(rect.Size()); pDC->SetViewportOrg(rect.TopLeft()); } CRect rect2 = ::CalcInstanceBoundingBox(ch, iInstance); pDC->SetWindowExt(rect2.Size()); pDC->SetWindowOrg(rect2.TopLeft()); ::RegisterCallback(pDC, _MoveTo, _LineTo); ::DisplayInstance(ch, iInstance); pDC->RestoreDC(-1); } void CHwbDlg::OnPaint() { CPaintDC dc(this); RenderInstance(&dc, m_chCur, m_iInstanceCur, GetOutputRect(), -10, TRUE); } void CHwbDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar*) { static int deltas[] = { -1, 1, -1, 1, 0, 0, -1000, 1000, 0 }; int iSel = nPos; if (nSBCode != SB_THUMBTRACK && nSBCode != SB_THUMBPOSITION) iSel = m_iInstanceCur + deltas[nSBCode]; if (iSel < 0) iSel = 0; else if (iSel >= m_nInstanceCount) iSel = m_nInstanceCount-1; m_imageBox.SetCurSel(iSel); OnInstanceSelect(); } // menu prompt string tracking void CHwbDlg::OnMenuSelect(UINT nItemID, UINT nFlags, HMENU) { CString strPrompt; if (nItemID != 0 && !(nFlags & (MF_SEPARATOR|MF_POPUP|MF_MENUBREAK|MF_MENUBARBREAK))) strPrompt.LoadString(nItemID); SetDlgItemText(IDC_STATUS, strPrompt); } // Line width: handle with one function for efficiency for a range of commands BOOL CHwbDlg::OnCommand(WPARAM wParam, LPARAM lParam) { int nWidth = LOWORD(wParam) - IDM_LINE_WIDTH_BASE; if (nWidth < 0 || nWidth >= MAX_LINE_WIDTH) return CModalDialog::OnCommand(wParam, lParam); // not line width m_nLineWidth = nWidth; // update the menu for (int i = 0; i < MAX_LINE_WIDTH; i++) GetMenu()->CheckMenuItem(IDM_LINE_WIDTH_BASE + i, (i == m_nLineWidth) ? MF_CHECKED : MF_UNCHECKED); InvalidateRect(GetOutputRect(), FALSE); // redraw return TRUE; } void CHwbDlg::OnLineColor() { CColorDialog dlg(m_colorCur); if (dlg.DoModal() != IDOK) return; m_colorCur = dlg.GetColor(); InvalidateRect(GetOutputRect(), FALSE); }
Copyright © 1992, Dr. Dobb's Journal
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.