home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
BUG 4
/
BUGCD1997_05.BIN
/
aplic
/
clip4win
/
clip4win.exe
/
C4W30E.HUF
/
CONVERT.TXT
< prev
next >
Wrap
Text File
|
1996-07-11
|
97KB
|
2,466 lines
Converting To Windows
=====================
This is about converting:
- developers (you!)
- applications
to Windows. Both imply understanding the Windows way of doing things,
which gets quite a bit of coverage below.
Before you read on, if you've not followed QUICK.TXT please do so now.
The Norton Guide (NG\CLIP4WIN.NG), Windows Help file (HLP\C4W.HLP) and
manuals contain further tutorials, e.g. on Object Orientation, and
a lot of other information.
Note: if you have an Evaluation Version of Clip-4-Win, you may have to
download extra files and/or purchase the manuals separately.
You may be wondering about the tools you use. Mostly you just use the
same ones as for DOS Clipper: the same version control system (if any);
the same editor; the same Clipper pre-processor/compiler (CLIPPER.EXE);
the same Norton Guides engine (NG.EXE or EH.EXE) - although you'll also
use the Windows Help engine (WINHELP.EXE). However, you need a linker
that can make Windows EXE files, which are not the same internally as
DOS EXE files, so unless you already use BLINKER, as many do, you need
one of the Windows linkers (BLINKER, MS LINK or OPTLINK).
Note: the Clip-4-Win Toolkit is an economical way to get a good linker.
You may also be able to get an evaluation version of Blinker (ask your
Clipper add-on supplier).
A tool that may well be new to you is a resource editor (and compiler).
You can use it visually to design forms (dialogs), bitmaps, icons, etc.,
via point and click, and then place these resources into your EXE file
(or a DLL) for use by your application as it runs. Resource editors
include MS AppStudio and Borland Resource Workshop. Form design is
particularly easy in resource editors. If it helps, think of a resource
editor as like a word processor or code generator for the user interface
parts of your applications. You may have seen screen designers in
products such as dBASE, FoxPro and Visual Basic.
Resource editors can often output an ASCII file (e.g. MYDLG.RC), a
binary file (MYDLG.RES), or can edit the resources in an EXE or DLL.
If you look at a few RC files, you'll soon see they use a simple
scripting language. An example appears later, with some explanations.
Being ASCII files, you can edit RC files with any text file editor in
addition to using a resource editor.
The Clip-4-Win Toolkit contains a resource editor as well as the Windows
linker.
Note: you do NOT need a resource editor to use Clip-4-Win, but you will
be able to design screens much more easily if you get one.
A tool you may have taken for granted is Windows itself. You can easily
open more than one DOS session, e.g. one for Norton Guides, another for
your editor and maybe another for RMAKE. You can Alt+Tab (or Ctrl+Esc
or mouse click) to switch between your sessions and/or Program Manager
(or Explorer or whatever). There's no need to keep shutting Windows
down.
The following sections give details of which parts of your DOS Clipper
applications you can keep and about how to convert the other parts.
It turns out that certain Clipper statements are much easier to convert
than others (as will be explained), so if you can start with an application
which has relatively few of the harder statements you'll be finished
sooner and gain confidence and valuable experience.
The code that can usually stay the same is your "business logic", i.e.
your code to create and maintain databases, check for existence or
non-existence of parts, customers, orders, and so on.
Your business logic is entirely or largely unconcerned with the user
interface, and is often over half the bulk of the code, counting by
lines of code. If anything, "lines of code" probably underestimates
the importance of your business logic, because developers of complex
applications eventually spend the bulk of their time creating, expanding
and debugging the business logic. Consequently, your business logic
may be much more than half your application in terms of complexity.
Being able to keep it is very important: you may save many months of
hard work. Also, if you structure the logic appropriately, you can
maintain just one set of code and use it in both Windows and DOS.
You can usually keep your network code (if any), although any parts
that interact with your user are going to need some revision.
This leaves the code involving the user interface, printing/reporting,
and your link scripts.
Generally speaking, in terms of conversion effort:
- menus are very easy
- box-drawing is usually very easy
- international character strings are very easy
- Clipper add-ons are usually very easy or else totally unsuitable
- link scripts are very easy
- changes to MAIN() are usually easy
- browsers are easy
- simple inputs using ACHOICE() or MEMOEDIT() are easy
- popup windows are usually easy
- data entry forms are less easy and also often need structural changes
- data driven is often quite easy
- printing/reports may be easy or hard
- converting from modal to non-modal (*) may be easy or hard
(*) "modal" is the typical DOS way: you have a very restricted number
of choices available in each of an application's screens, and to access
the other parts of the application you have to complete or cancel the
current screen. MS encourages less modality in Windows applications.
Please refer to the topic "Converting From Modal To Modeless", which also
discusses "SDI" and "MDI".
Note that menus in DOS Clipper do not have to be implemented using
@ PROMPT ... MENU TO. That doesn't matter when converting, what does
matter is that you identify the underlying functionality.
Similarly, data entry isn't always done using @ ... SAY ... GET ... READ,
but may instead use INKEY() etc. Regardless of the actual implementation
you have in DOS, you're almost certain to end up with a particular kind
of (different) code in Windows.
Why? Because Windows is different to DOS and does things in different
ways. Don't be too concerned, however, as Clip-4-Win greatly reduces
your pain. For example, as well as keeping your business logic you get
some very easy to use menus. And you get a browser that's very like
TBROWSE yet also supports non-proportional fonts, bitmaps, scroll bars,
the mouse, re-sizing, drag-and-drop columns, and so on.
Clip-4-Win helps in another way: even the code you will eventually
convert can usually be left temporarily as it is. Generally you will
be able to link your application and run it in Windows, although it's
not a good idea to invoke the unconverted code. Being able to convert
some of the code and leave large amounts unconverted means you can do
the conversion mostly in the order that suits you.
Just as you can implement each part of your DOS applications in various
ways, so you can in Windows. However, unlike in DOS, many things are
fairly well standardised in Windows. You probably want your applications
to look and feel like they belong in Windows - so be very cautious about
doing things in non-standard ways. It usually means more work, sometimes
a lot more work, too.
This is a good place to stress that you really should think hard before
you decide to do things your own way rather than the standard Windows
way. Even if you think your way is better, you are likely to be severely
criticised by experienced Windows users, reviewers of your product and
your competitors. If you don't currently use any Windows applications
in your work, switch over to a few if you can, so you get used to what
people will expect from your applications.
Consider reading "Windows Interface Guidelines for Software Design"
(Microsoft Press, ISBN 1-55615-679-0, July 1995, 300 pages).
Another source of information, highly recommended, is the MS Developer
Network (on CD-ROM), often known simply as "MSDN". Currently, this is
available as a subscription service at various levels. If you're short
of money, just go for the cheapest (Single Edition, if you can get it,
or Level 1), which includes the above book and a truly huge amount of
other information. Otherwise, at least get the Professional (Level 2,
which includes lots of SDKs). Only get the most expensive subscription
if you're sure you need it.
MSDN includes the Interface Guidelines referenced above.
For information about books, see the HLP file topic "Windows Books" or
the NG topic Other...Windows Books.
Let's examine each of the parts of a typical Clipper application, and
see how they're done in Windows using Clip-4-Win. Of course, it may be
that your application isn't at all a typical Clipper one, as Clipper is
flexible enough to be useful in all sorts of applications. If that's
the case, apologies are due here, but maybe some of the following will
still be relevant.
Before looking at the details of converting your code there's the issue
of the "style" you like to use: not the way you indent or use upper/lower
case, but your preference for using commands (Xbase syntax) or functions,
and your liking or not of objects.
You can program Clip-4-Win in the same sort of style you use for DOS
Clipper: avoiding UDC's or using lots of them, and few or many objects.
If you use a class-creation add-on such as Class(y), you can continue
using it. Clip-4-Win doesn't make you change, and it doesn't force you
to use objects - though Clip-4-Win has some powerful new classes.
In the details to follow, you'll find information on various ways to do
things, so you should be able to stick to your preferred style. As with
DOS Clipper, you can freely mix styles (just about everything ends up as
a function call).
You may be aware of "standard" ways of programming Windows, e.g. with
a GetMessage() loop. You can program Clip-4-Win like that if you want,
perhaps because you've got a book about programming for Windows and like
its approach, or you can leave Clip-4-Win to handle such things. Again,
this is really a personal preference. There are many samples you can
look at to see which style you like, and the details to come should help,
too.
For that matter, you may have heard of MFC (Microsoft Foundation Classes).
Clip-4-Win's Application Classes (included with the product) are the
nearest equivalent, but deliberately easier to use and oriented towards
business applications.
A small aside: some people sneer at others wanting to use their own
message loop and access the Windows API directly. This seems an odd
sort of arrogance, not least because that style means the many excellent
books about Windows are most useful. The same sneering developers tend
to say "you might as well use C", conveniently forgetting that Clipper
has many powerful features not in C, such as its database (Xbase)
handling, its ragged arrays, string handling, and so on.
As mentioned before, you can program in any of the above styles, and
you can mix them. For that matter, if you ever wish to do so, you can
convert between styles quite easily. You will probably never do so,
unless perhaps you start out not using objects and then decide to move
over to objects.
First let's look at the major functional parts of an application, then
at the underlying Clipper statements one by one.
Converting MAIN()
-----------------
Before MAIN() itself, you are likely to want at least:
#include "windows.ch"
#define NO_C4WCLASS // leave this line out if using objects
#include "commands.ch" // leave this line out if not using UDCs
/*
* Add the following if you want to create any objects using the
* MyClass{ <params> }
* syntax:
*/
#include "topclass.ch"
At the start of MAIN(), if you're avoiding UDCs and avoiding objects,
you usually want:
SET CONFIRM ON
SET SCOREBOARD OFF
/*
* The C4W_AutoClose() function allows us to make the user confirm
* their decision to quit the app, even if they try to close a window
* with ALT-F4 (or via the System menu).
*/
C4W_AutoClose(.F.) // disables the auto-closing of windows
/*
* Use the following if you want Windows to have any idle time.
* Leaving this out may disable screen savers and/or power
* management features, and may result in 100% CPU utilisation.
* In practice, you want this line of code unless you've got a
* special reason of your own to leave it out.
*/
C4W_UsePeekMessage(.F.)
/*
* You're almost certain to need SetHandleCount( <n> ) !!
* (As well as the other ways to let Clipper use many file handles.)
*/
SetHandleCount(50) // the default is only 20
/*
* Use the following if you want 3-D effects on versions of Windows
* that don't automatically provide 3-D. Note: you also need to
* ship CTL3D.DLL with your application.
*
* If you use this, you should try to call Ctl3D(.F.) just before
* your application does a QUIT (to unload the DLL).
*/
Ctl3D(.T.) // implemented in SOURCE\APPMAIN.PRG
/*
* Use the following if you intend to use any Borland-style dialog
* items in your application. Note: you then also need to ship
* BWCC.DLL with your application.
*
* If you use this, you should try to call BWCC(.F.) just before
* your application does a QUIT (to unload the DLL).
*/
BWCC(.T.) // implemented in SOURCE\APPMAIN.PRG
The above are done automatically by the UDC (CREATE APPLICATION) to be
used below. The relevant ones are also done for an OO application by
the WAppBase application base class, in method WAppBase:Init().
Note: the defaults for CREATE APPLICATION and WAppBase:Init() are to
use 3-D but not load BWCC.DLL.
The 3-D effects are not strictly needed on Windows 95 (or NT 4). You
can even argue that they're wrong there, as the 3-D look is slightly
different. See "Converting Data Entry Forms" for more information.
Next you want any of your own initialisation code that should run
before you create your main window. This does not include a menu, as
a menu has to be attached to a window. For examples of splash screens,
see SOURCE\OO\SPLASH.PRG and SOURCE\SPLASH2.PRG.
After this initialisation code you can create your main window and
optionally add a menu (you're not forced to use a menu). You can create
your window using UDC's or function calls, and avoiding or using objects.
As stated before, you can use the style you prefer.
For example:
LOCAL oApp, oWnd
SET CONFIRM ON
. . . your init code . . .
CREATE APPLICATION oApp WINDOW oWnd TITLE "Clip-4-Win Example" ;
ON INIT DoInit(oWnd)
. . .
STATIC FUNCTION DoInit(oWnd)
/*
* You're almost certain to need SetHandleCount( <n> ) !!
* (As well as the other ways to let Clipper use many file handles.)
*/
SetHandleCount(60) // the Windows default is only 20
MenuSetup(oWnd)
ToolbarSetup(oWnd)
. . .
CREATE APPLICATION (from COMMANDS.CH) arranges to create the window
before actually calling DoInit(). Although the fine details of CREATE
APPLICATION may change in future versions of Clip-4-Win, in a non-OO
application this UDC:
- calls function C4W_AppInit() [in SOURCE\APPMAIN.PRG]
- creates the main (frame) window, using the Windows API function
CreateWindow() [described in the Clip-4-Win NG/HLP/manuals]
- calls the "ON INIT" function, if any [here it's DoInit()]
- calls function C4W_AppMain() [also in SOURCE\APPMAIN.PRG], which
contains the "message loop" mentioned before
The exact details are different for an OO application, but the
underlying functionality is the same. CREATE APPLICATION (if used),
does this:
- creates an application object (oApp)
- creates a main (frame) window object (oWnd)
- calls the "ON INIT" function, if any [here it's DoInit()]
- starts the message loop, using oApp:Start()
Don't be too frightened to look at COMMANDS.CH: CREATE APPLICATION
is one of the simpler UDC's.
For a longer sample using UDC's, see SOURCE\APP.PRG. The version
without UDC's is SOURCE\APPOLD.PRG.
For an OO sample, try the OO tutorial in the NG, HLP and manuals or
the many samples in the SOURCE\OO directory.
Before your application finally exits, it should free any DLL it has
loaded. This is partly to allow Windows to re-use the memory if it
needs it, but also to let a different version of the DLL be loaded, in
case you have an application that depends on a particular version.
You can make sure CTL3D.DLL and/or BWCC.DLL are unloaded using:
Ctl3D(.F.)
BWCC(.F.)
Both functions ignore any extra calls, so you can call them before any
QUIT, e.g. in ERRORSYS.
During development it's easy to leave a DLL in memory by accident. If
you're not changing it for a different version, having it left in memory
doesn't really matter. However, if you need to unload a DLL, e.g. so
you can use a newer version, you can try SOURCE\DLLFREE.PRG. If you
still have problems, restart Windows.
Converting Menus
----------------
As stated, converting menus - whether implemented in DOS using MENU TO
or any other way - is basically very easy. However, there is a minor
complication.
In DOS you often have sub-menus that are scattered around your code,
and are only seen by the user if they choose the relevant menu items
that lead to the sub-menus. You may restrict access to certain menus
or menu items, depending on such things as login name/password or that
some other activity has taken place previously.
You can achieve something roughly equivalent in Windows; the minor
complication is that you should describe all your menus and sub-menus
at once. This lets Windows handle your user's menu navigation and
selection, whether by mouse, keyboard, pen, or a combination.
One way you can restrict access is to gray (disable) some items, which
has the advantage of showing your user that they're potentially available,
but some other condition needs to be met first. Naturally, you may not
want some options to appear at all if they relate to security or if
keeping the menu as simple as possible is your concern.
Another way you can restrict access is to use different menus/sub-menus
depending on the login name/password or some other information.
However, you should normally set the menu once and not keep changing
its structure dramatically. As your application runs, you can disable
and enable items, but avoid even that if you can, as users tend to find
it frustrating/confusing. It often makes sense to be able to choose at
any time a menu item that opens another window.
Frequently, only your top-level (frame) window has a menu, which you may
modify depending on the other windows your user has open. You can have
menus on other windows if it seems a particularly good idea. Keep in
mind that you may confuse your users if you use more menus, that it's
more work for you, and that a menu reduces the window area in which you
can display other information (the "client rectangle").
Dialogs are a particular kind of window used to get input from users,
and don't normally have a menu of their own. Indeed, certain dialog
styles cannot have a menu. The former is a user interface issue, the
latter is a restriction of Windows. For much more information about
dialogs, see "Converting Data Entry Forms".
Converting Menus - An Example
-----------------------------
Let's suppose you have an application that users must log into. The
main menu is displayed in function Main(). There's a sub-menu in
function Customers(). There's another sub-menu in Admin() that's only
visible if IsAdminUser() says it should be (by returning true).
The essential elements of the code are:
function Main()
. . .
. . . init and login code . . .
. . .
@ Row()+1,nLeft PROMPT "Customers"
. . .
if IsAdminUser()
@ Row()+1,nLeft PROMPT "Admin"
endif
MENU TO nChoice
do case
case nChoice == 1
Customers()
. . .
case nChoice == 5
Admin()
. . .
return nil
function Customers()
. . .
@ Row()+1,nLeft PROMPT aCustMenu[1]
. . .
MENU TO nChoice
. . .
do case
case nChoice == 1
CustFind()
. . .
return nil
function Admin()
. . .
@ Row()+1,nLeft PROMPT aAdminMenu[1]
. . .
MENU TO nChoice
. . .
return nil
As previously stated, you need to combine all the menus into one. You
make each sub-menu a POPUP menu, and each other item a MENUITEM.
In Windows, POPUP menus are either in a bar menu or in another POPUP
menu. A MENUITEM can be in a POPUP menu, or (uncommonly) in a bar menu.
You can navigate through a POPUP, if it's not grayed/disabled, but you
can only select a MENUITEM. (If you've never used Windows and haven't
followed QUICK.TXT, you shouldn't be reading this!)
Windows uses a "handle" to identify uniquely each window or menu, or
indeed each other item - whether a user interface item or a non-user
interface item. This is essentially the same way that you use objects
in Clipper for each GET, TBROWSE or TBCOLUMN. Names of handles usually
begin with the letter "h", e.g. hWnd, hMenu, hDLL. Sometimes they're
shown as oWnd, oMenu, and so on, perhaps to make a potential move to
an OO application easier or to use a prefix more familiar to Clipper
developers.
Handles are nothing unusual: you've certainly met file handles in DOS
and you may also have met handles in other products, such as CA-Tools.
Ignoring for the moment whether it's a good Windows user interface
(there's not enough here to be able to judge), here's a direct
translation, using simple UDC's but no objects:
#include "windows.ch"
#define NO_C4WCLASS // leave this line out if using objects
#include "commands.ch"
function Main()
. . .
. . . init and login code . . .
. . .
return nil
static function MenuSetup(hWnd)
MENU IN hWnd
POPUP "&Customers"
MENUITEM aCustMenu[1] ACTION CustFind()
. . .
ENDPOPUP
. . .
if IsAdminUser()
POPUP "&Admin"
MENUITEM aAdminMenu[1] ACTION AdminXXX()
. . .
ENDPOPUP
endif
ENDMENU
return nil
Please see the section "Converting MAIN()" for details of its call to
MenuSetup().
Notice that the nChoice variables and associated DO CASE statements are
no longer needed, and that functions Customers() and Admin() are also no
longer needed.
The "&" character makes the following character an accelerator (hotkey)
and indicates this to your user by underlining it.
An object-oriented version is almost identical, but you would probably
use a method instead of the static function, and "hWnd" will be "oWnd"
(or "self", for a method).
Here's MenuSetup() implemented using function calls:
static function MenuSetup(hWnd)
local hMenu, hPopup
hMenu = CreateMenu()
hPopup = CreatePopupMenu()
AppendMenu(hMenu, "customers", MF_POPUP, "&Customers", hPopup)
AppendMenu(hPopup, "custfind", MF_STRING, aCustMenu[1], {|| CustFind()})
. . .
if IsAdminUser()
hPopup = CreatePopupMenu()
AppendMenu(hMenu, "admin", MF_POPUP, "&Admin, hPopup)
AppendMenu(hPopup, "admin1", MF_STRING, aAdminMenu[1], {|| AdminXXX()})
. . .
endif
SetMenu(hWnd, hMenu)
return nil
Converting International Character Strings
------------------------------------------
Windows uses a version of the ANSI character set, whereas DOS uses what
Windows calls the OEM character set. This can cause problems with both
box-drawing and international (e.g. accented) characters.
Box-drawing characters should usually be removed and a dialog or menu
used instead, but if you really needed to draw boxes you can easily do
so in Windows, e.g. using MoveTo() and LineTo().
You should keep ANSI characters out of database files and file names.
However, you want ANSI characters in window/dialog titles, calls to
MessageBox() and controls. Windows provides the OemToAnsi() and
AnsiToOem() functions, and the CBS_OEMCONVERT and ES_OEMCONVERT styles
for combo boxes and edit controls.
Converting:
OEM -> ANSI -> OEM
or
ANSI -> OEM -> ANSI
does not always result in the original string, because some characters
have no equivalent. Don't blame Clip-4-Win!
Also, some fonts cannot display certain characters. This is a feature
(limitation) of the fonts. Sorry: it is not Clip-4-Win's fault.
When using a resource editor, the characters you enter are ANSI ones.
DOS text editors usually treat input as OEM characters. This means if
you have any special characters (e.g. ASCII value > 128) in strings in
your program code, you need to use OemToAnsi() to display those strings
to your users (in menus, dialogs, message boxes etc.).
Windows text editors can sometimes be set to use either character set,
but otherwise normally treat your keystrokes as ANSI.
If you're not sure what you have in a file, try viewing it with a Windows
tool such as NOTEPAD, and you'll see your characters treated as ANSI. If
they look right, they are ANSI. If not, they're OEM characters, so you
probably need to use OemToAnsi().
The Clipper compiler doesn't care whether you use OEM or ANSI characters
in strings; it just compiles what it finds. What matters is what you
use the strings for at runtime.
For example,
MessageBox( , OemToAnsi(CHR(129)), "u umlaut")
and
MessageBox( , CHR(252), "u umlaut")
and
MessageBox( , OemToAnsi("ü"), "u umlaut")
all do the same thing, but the last one will look wrong if you don't
view this file with a tool which uses the DOS (OEM) character set.
You can store ANSI strings as string resources in your EXE or a DLL,
and your application can fetch them using LoadString(). If you have
to support more than one language, consider using a separate DLL for
each language. If you use a separate DLL for each language and use the
same ID for each translation of a string, a single LoadString() in
your code is all you need, regardless of the language. Your code can
select the appropriate DLL once, and then the rest of your code need
not be dependent on the language.
For example, you might keep the name of the appropriate DLL in an INI
file, with the same name as the EXE (but INI as the extension), stored
where the EXE resides, and proceed like this:
static hDLL
function InitLanguage() // the once-only init code
local cEXE := GetModuleFileName() // where the EXE lives
local cINI := left(cEXE, rat(".", cEXE)) + "INI"
local cLang := GetPvProfString("Init", "Language", "ENG", cINI)
if (hDLL := LoadLibrary(cLang + ".DLL")) <= 32
// error ...
endif
return nil
function LoadStr(nId)
return LoadString(hDLL, nId)
exit function Cleanup()
FreeLibrary(hDLL)
return nil
The various Windows API functions used above are described in the
Clip-4-Win documentation.
Notice that the above code is very generic. You can support another
language in the future just by adding another DLL and changing the INI
file.
You can maintain these language-specific DLLs using a resource editor,
which has the advantage that you don't need a programmer for the job.
Of course, you can also store strings in database files, but then you
need some specific software to maintain them.
Note: a right-to-left language (e.g. Arabic or Hebrew) or one using
pictograms/ideograms is beyond the scope of this discussion.
Converting Clipper Add-On Code
------------------------------
Clipper add-ons which are well-behaved in protected mode usually work,
providing they don't rely on low-level things like I/O port accessing,
hardware interrupts, or screen RAM.
Well-behaved add-ons are believed to include: ADS, Blinker 3 or 4,
Class(y), CLGraph/3D, CLImage, ClipWKS, COMIX, dbClass, Ed, FlexFile,
FUNCky (not the screen I/O), HighClass, some of Nanforum and Netto,
NetLib, NovLib, ObjectDB, OClip (with care can even be used as well as
TopClass), RaSQL/B, SIx Driver (and related RDD's), TechWriter.
NOTE: For NovLib 2 you need to call NLWinApp() before any other NovLib
function.
RTLink (as shipped with Clipper) and ExoSpace are not relevant or
needed, because they cannot create Windows EXE files. Clip-4-Win uses
Windows as a protected mode extender.
Badly-behaved add-ons include: CA-TOOLS, dGE (use Graphics Server),
Telepathy (use Silverware).
Windows add-ons include: almost anything that's a DLL, AppStudio,
Borland Resource Workshop, BTrieve, CA-RET / CA-Visual Express,
Crystal Reports, Graphics Server, products supporting DDE, MS Excel,
MS LINK, MS ODBC, MS Word for Windows (WinWord), Q+E Database Lib,
OptLink, R & R for Windows, ReportSmith, SofToolS, SOS Info Author,
Strike! for Windows.
Converting Link Scripts
-----------------------
There's no need to overlay, so you can remove anything to do with that.
Forget all about "load size", it's irrelevant in Windows. Get used to
the idea that Clipper applications are huge DOS programs but Clip-4-Win
applications are quite small Windows programs. If your EXE is 2MB it
doesn't matter to Windows.
You need to use a DEF file (or in Blinker DEFBEGIN ... DEFEND) because
it's the only thing that makes the linker build a Windows EXE file.
The SOURCE directory contains a number of sample DEF files. You can
just use CLIP4WIN.DEF, but you may well need a larger stacksize if you
have a complex application and/or you nest function calls deeply. Try
going up 500 bytes at a time. You may also need to increase heapsize,
in which case try similar steps. Unfortunately, there's no simple way
to tell whether you're having a stack/heap problem or something else.
Few applications need more than 14000 for stacksize. Heapsize rarely
needs to be more than a few thousand at the most. Keep both of these
as small as you reasonably can, as they come from Clipper's scarce
DGROUP - which is limited to 64KB.
If you make your own DEF file, note that the "name" is limited to 8
characters. Also, don't change the exetype unless you're sure you know
what you're doing. E.g. do not use 3.11, stick to 3.1.
Don't worry about most of what's in CLIP4WIN.DEF - just use it.
MS LINK uses the equivalent of RTLink's positional syntax. If you need
to use multiple lines, place a "+" character at the end of each line
before the final line.
Whichever linker you use, list RDD's and most add-ons before Clip-4-Win's
libraries, and if you list any of Clipper's libraries put them last.
Converting Box-Drawing
----------------------
See "Converting International Character Strings".
Converting Browsers
-------------------
Although you can use TBROWSE in Windows, it does not allow you to use
the new facilities provided by Windows. So you can't use proportional
fonts, bitmaps, Windows scroll bars, etc. Using WBROWSE instead of
TBROWSE gives you many new features.
You can convert your TBROWSE code to WBROWSE largely by removing your
entire stabilize loop. It's now effectively internal to WBROWSE, and
has some new code blocks e.g. KeyBlock, FindBlock and ScanBlock. You
may well want to set the Escape or other properties (instance variables).
You need to change from TBCOLUMNNEW() to WBCOLUMN{}, and TBROWSENEW()
or TBROWSEDB() to WBROWSE{}. See the comment in "Converting MAIN()"
about #include "topclass.ch".
Note: WBROWSE{} needs more parameters than TBROWSEDB()/TBROWSENEW() and
in a different order, consistent with Windows. WBCOLUMN{} allows
more parameters than TBCOLUMNNEW(), but the first 2 are enough to
start with.
If you use multi-line rows/headers, they're much easier to do using
WBROWSE: for a column heading you use an array (one element for each
line), and for a cell your column block only has to return a string with
carriage return and linefeed (CHR(13) + CHR(10)) between lines.
You may have to control the vertical scroll bar yourself, e.g. for an
array. You can do this in your SkipBlock, as shown in the samples
named below and in the HLP file.
Please see the WinHelp file HLP\WBROWSET.HLP and the many samples using
WBROWSE. These include SOURCE\WBROWDEF.PRG, the in-depth demonstration
SOURCE\WBTDEMO.ZIP, and in SOURCE\OO\ there are BROW.PRG, MDIWBROW.PRG,
ODBCBROW.PRG and ODBCWBRO.PRG.
This is perhaps a suitable point to say just how powerful WBROWSE is.
It really makes good use of the GUI environment. For example, you can
allow your user to re-size columns with the mouse, or re-arrange the
columns by dragging and dropping them. Build SOURCE\WBTDEMO.ZIP and
see for yourself!
A special feature of WBROWSE is that you can use it in a dialog where
a Windows LISTBOX control would otherwise be used. By doing this you
can overcome various limitations of list boxes, e.g. no right-aligned
or centered items and serious problems handling more than 64K items.
If you want to do in-cell editing, as commonly done in spreadsheets such
as Excel, SOURCE\WBTDEMO.ZIP has an example (CELLEDIT.PRG).
Converting ACHOICE() and MEMOEDIT()
-----------------------------------
You can probably use SOURCE\ACHOICE.PRG and SOURCE\MEMOEDIT.PRG without
any changes, but you can make your own version of the code if you need
something different. Note: the "user function" UDF's are not supported
(the parameters are ignored). If you really need something similar,
see "Converting Data Entry Forms".
Converting Popup Windows
------------------------
In each case, identify the underlying functionality. Menus, browsers,
and data entry forms are discussed separately. That leaves things
like informational/warning/error messages. An easy way to implement
these is to use a MessageBox(). You might also take a look to see if
SOURCE\ALERT.PRG is suitable for your needs.
Converting Data Entry Forms
---------------------------
There's a lot of detail in this section and the related sections
"Converting Forms - An Example" and "Validation". Please plan to read
them all, and more than once! Quite a few samples are discussed; you
might find it worthwhile to print them for easier reference.
Data input screens normally need converting to dialogs. If you have a
multi-page form, you might like to use a tabbed dialog (class oTab).
See HLP\OTABT.HLP and SOURCE\OTABDEMO.ZIP.
A dialog is special kind of window built from a template. Clip-4-Win
supports dynamic dialogs, where the template is created at runtime, as
well as resource dialogs, which are constructed in advance using a
resource editor and then stored in the EXE or in a DLL.
Dialogs contain other kinds of windows, called controls: push buttons,
radio buttons, check boxes, list boxes, combo boxes, "static" text
controls, group boxes, edit controls (input fields), and so on.
Don't be fooled - static controls are badly named: you can change the
text at any time. You can also use a static control to display an icon.
In case you discover owner-drawn buttons, these are also not brilliantly
named: you can use them for just about anything you might want to do.
If you want to know more, refer to the sources of information about
Windows described previously. You could also take a look at several of
the classes provided by Clip-4-Win, which use owner-drawn buttons as a
convenient way to acquire a rectangular area for their own use. In the
SOURCE\OO\CLASSES directory, see ARROWBTN.PRG, BITMAP.PRG, BUTTNBMP.PRG,
PROGBAR.PRG and SPINBTN.PRG.
Most kinds of windows, including dialogs and controls, can be created
with a variety of settings, called styles. The styles that apply to
most windows are the WS_* values defined in WINDOWS.CH (e.g. WS_VISIBLE
and WS_DISABLED). You can use these styles with controls and each type
of control also has its own specific styles. E.g. an edit control uses
ES_PASSWORD to mean that it should display each input character using a
replacement character (default '*'). Similarly, the presence or absence
of ES_MULTILINE is the difference between a single-line input field and
one you could use for a memo.
A resource editor is an easy way to choose the right styles for dialogs
and controls for a number of reasons:
- a sensible default is provided
- you can choose from just the applicable alternatives
- you can change styles very easily (e.g. point and click)
- you see the effect of any changes immediately
You use a WBROWSE in a dialog by specifying an owner-drawn listbox. Do
this by choosing a listbox and then be sure to use styles:
LBS_OWNERDRAWVARIABLE, LBS_NOINTEGRALHEIGHT and also LBS_NOTIFY.
The exact way you choose the style depends on the particular resource
editor you use; typically you can use check boxes with names similar
to the above. For a non-resource (dynamic) dialog, just add the above
values to typical ones for a listbox, e.g.
LBS_OWNERDRAWVARIABLE + LBS_NOINTEGRALHEIGHT + LBS_NOTIFY
+ WS_TABSTOP + WS_CLIPCHILDREN + WS_VISIBLE + WS_CHILD
(The "@ ID ... BLISTBOX" UDC will do this for you.)
If you want to have 3-D effects in a version of Windows that doesn't
automatically supply them (i.e. Win3.1/3.11, Workgroups, OS/2, NT 3.x),
you can use CTL3D.DLL. You can do this as described in "Converting
MAIN()", and your application's dialogs will then be given a 3-D look.
The CREATE APPLICATION UDC and the WAppBase class default to enabling
this 3-D look.
In Windows 95 and NT 4, you can either use the above 3-D effects or you
can use the native effects, which have a slightly different appearance.
To use the in-built 3-D, instead of using CTL3D.DLL you need to add 4
(four) to each dialog style. Hint: you may like to use:
#ifndef DS_3DLOOK
#define DS_3DLOOK 4
#endif
You can safely add this to dialogs for use with any version of Windows,
as it's ignored by earlier versions.
In 95/NT4, you may want to specify a non-bold font for each dialog.
We intend to make the above easier in a future release of Clip-4-Win!
If you use the Borland Resource Workshop to design your dialogs, you
may well choose one or more items specific to Borland (such as their
bitmapped buttons). If you do, you need to ship BWCC.DLL with your
application and at runtime your application needs to load BWCC.DLL into
memory before it uses any of the dialogs with Borland effects.
You can do this with CREATE APPLICATION just by using the optional
clause BORLAND (or BWCC).
An OO application can use:
oApp:BWCC := .T. // an ASSIGN method in WAppBase
You can load the DLL yourself like this:
BWCC(.T.) // in SOURCE\APPMAIN.PRG
Before your application finally exits, it should free any DLL it has
loaded. This is partly to allow Windows to re-use the memory if it
needs it, but also to let a different version of the DLL be loaded, in
case you have an application that depends on a particular version. To
remove an unwanted DLL, see the description of DLLFREE in "Converting
MAIN()".
Dialogs can be modal or modeless (non-modal). A modal dialog is one
your user must either finish or cancel; a modeless dialog lets your
user switch to (or try to start or try to end) other activities within
the same application. The function call used to invoke the dialog is
what determines whether a dialog is modal or modeless.
A modal dialog is usually application modal, which means the rest of
the application is disabled including its menu (if any) and any other
dialogs. However, the user can still switch to other applications.
You can also use a system modal dialog. If you do, the rest of Windows
is disabled during the dialog! You will probably never use a system
modal dialog, but if you do be sure to test it very thoroughly. Start
by testing it as an ordinary application modal dialog, and only later
add the style DS_SYSMODAL.
Converting modal DOS forms to modeless dialogs is discussed in the
topic "Converting From Modal To Modeless". It's not something to try
to do if you're new to Windows programming.
If you used non-modal screens in DOS Clipper, keeping them non-modal in
Windows is simple. You may well have such modeless DOS screens if you
used a DOS Clipper add-on such as ProVision:Windows, CLWindows, CUA
Bausteine 3 or if you wrote your own event-driven system. You may have
been influenced by Clipper "gurus" such as Gilbert Chauvaux and Erik
Wynn. And you're probably finding this discussion tedious - sorry!
Designing pleasant-looking dialogs with a resource editor is reasonably
easy. It is considerably harder without a resource editor.
When you're designing a dialog, you're doing two things: deciding the
user interface and the business logic associated with it. These two
are related, but many of the details affect only one or other of the
two. In particular, the position of each control is usually irrelevant
to the logic. This brings some advantages:
- you can design the dialogs fully, but write little code
(e.g. to see if your user likes the proposed look)
- design a dialog very quickly and then concentrate on getting the
logic right
- have some people who program and some who design dialogs (who need
not be developers)
Some of the commonest ways of using controls are discussed next, but
first you need to know a little about handles and ID's.
Every window on the screen, including each dialog and each control, has
a "handle" which is used by Windows to uniquely identify the window.
Handles are stored as Clipper numerics. In fact, Windows also uses
handles for fonts, brushes, menus, pens, and other things. Handles
your application creates are never zero. Handles are allocated by
Windows when your application requests them, and are usually different
each time a program is run.
Each control in a dialog has a numeric (16-bit) ID, which you or the
resource editor choose. Although it is possible to change ID's each
time an application is run, it is almost never done because it is so
rarely useful. Instead, each ID is kept constant and the same value
is used in the resource file (if any) and at runtime. The ID may also
be used to find the window handle of a control.
For example, if hDlg is the handle of a dialog and you have a typical OK
button, its ID is IDOK. You can get the window handle of the OK button
using hCtrl := GetDlgItem(hDlg, IDOK). You could disable it using
EnableWindow(GetDlgItem(hDlg, IDOK), .F.), and it would then be drawn
grey.
Similarly, you could disable a list box whose ID is ID_XXX using
EnableWindow(GetDlgItem(hDlg, ID_XXX), .F.). You can do this sort of
thing with any of the standard controls.
Developers often set the ID's of controls which aren't referenced in
their code to -1 (in 16-bits this is the same as 65535).
You can convert from a control's window handle to its ID using
GetDlgCtrlID(hCtrl).
As well as the above ways to use control ID's at runtime, you use them
when creating a control and when associating an edit control with a
Clipper GET. A detailed example follows, containing things like:
@ ID 101 EDIT AT 60,10 SIZE 24,14
. . .
@ DIALOG hDlg ID 101 GET cZip PICTURE "@!"
When stored as a resource a dialog also has an ID, but this is only
used at runtime to find the dialog in the EXE/DLL. A dialog ID may be
either a string or a (16-bit) numeric, which should not be zero.
E.g.
CREATE DIALOG aDlg RESOURCE "ZIP"
The way the Tab key moves around a dialog (the "tab order") is decided
by which controls have the WS_TABSTOP style and the order of those
controls within the dialog. This is the order in the RC file or the
order in which the controls were created using "@ ... ID" or
AppendDialog(). If you're having trouble with the tab order of a
resource dialog, remember you can edit the RC file!
When the dialog is displayed, the first control with WS_TABSTOP is by
default given the input focus.
As far as the user interface is concerned, the commonest ways of using
controls are as follows.
You usually use a static text control instead of @ ... SAY, but a group
box is sometimes appropriate (it can have text as well as its rectangle).
As with menus, you can use "&" before a character to make it an
accelerator (hotkey). This is useful in radio buttons, check boxes,
buttons and so on as the user can use a simple keystroke instead of the
mouse or having to tab around. If you use an accelerator in static text
placed before an edit control, the input focus will move to the edit
control if the accelerator is used.
GETs which have only two valid inputs can probably become check boxes.
GETs which have a small number of different inputs can usually become
check boxes, radio buttons, or a list box. A drop down list is
implemented using a combo box with the CBS_DROPDOWN or CBS_DROPDOWNLIST
style.
All your multi-character GETs are likely to end up as edit controls,
with most using @ ID ... GET or @ DIALOG ... GET. These allow PICTURE
clauses. See below for a discussion of validation.
Multi-line input (a memo) can use a multi-line edit control. These
automatically support the mouse, clipboard, and so on. If you want
the Enter key to start a new line (instead of terminating the dialog),
use the ES_WANTRETURN style.
If you think you have so much to input within a single dialog that you
need scroll bars, try using a tabbed dialog (property sheet). See
HLP\OTABT.HLP and SOURCE\OTABDEMO.ZIP.
As well as deciding the appearance of a dialog, you have to decide the
details of the code (your business logic). This gets much easier as
you gain experience - but the first time can be rather daunting!
It helps to understand the major states a dialog can be in and the
important events that affect a dialog:
(a) Initially, the dialog is stored as a resource or has yet to be
created dynamically.
(b) The resource is fetched into memory from the EXE or a DLL, or your
application builds a dynamic dialog. At this stage, no window handles
exist. Any problem with a resource dialog (e.g. not in the EXE or you
use an incorrect ID) causes an error return. For a modal dialog, e.g.
using SHOW DIALOG or DialogBox(), the error code is -1. For a modeless
dialog, e.g. using SHOW DIALOG or CreateDialog(), the error code is 0.
(c) Windows/Clip-4-Win create the dialog window and the controls.
(d) If no error occurred, the dialog is made visible.
(e) Your user interacts with the dialog.
(f) The dialog is ended. The windows are destroyed and the handles
cease to be valid.
You can write code to influence almost any of the above.
Windows keeps your application informed about the things that occur by
sending it special messages (some of the WM_* values defined in
WINDOWS.CH). Your application can perform some of its own processing
as it receives these messages. You very rarely handle more than a few
of the messages. The ones you commonly handle are very important and
are discussed next.
The messages are sent by Windows to your "dialog procedure", a function
to process them. Clip-4-Win provides several built-in dialog procedures,
e.g. in the WDialog class, and in the support code for SHOW DIALOG. You
can also write your own dialog procedures. E.g. SOURCE\DROP.PRG and
SOURCE\USERSRES\USERSRES.PRG.
Note: this section discusses dialog procedures, not "window procedures",
because in Clip-4-Win you will probably never need to use a window
procedure. Just be aware that something more generic exists and applies
to all kinds of windows.
You need to know at least a little about Windows messages, because they
are so incredibly important in making things work. You don't need to
learn what all the different messages do, but you do need to learn what
a few do and when they occur.
You can imagine messages as being like keystrokes in DOS Clipper, and
a dialog procedure as being like a SET KEY procedure. Alternatively,
you can say messages are like keystrokes in DOS and a dialog procedure
is like the "user function" UDF used with ACHOICE, DBEDIT and MEMOEDIT.
Each message consists of four values:
- the handle of the receiving window (or dialog or control: remember
they're all kinds of windows), often called hWnd, hDlg or hCtrl
- the message (a 16-bit number, one of the WM_* values in WINDOWS.CH),
usually called nMsg or msg
- a 16-bit additional parameter, usually called nwParam or wparam
- a 32-bit additional parameter, usually called nlParam or lparam
These are all Clipper numerics in Clip-4-Win. Unused values are zero.
The Clip-4-Win Application Classes use methods with names derived from
message names, e.g. OnCommand() for WM_COMMAND, to make things easy to
find.
The two most important messages are:
- WM_INITDIALOG nwParam is the handle of the control to get the
input focus (you usually ignore this)
nlParam can be ignored
- WM_COMMAND nwParam is the ID of the control sending the
message e.g. IDOK for an OK button
nlParam holds two values you can access as:
C4W_LoWord(nlParam) = the control's handle
C4W_HiWord(nlParam) = the notification code
(normally ignore this)
Despite their name, dialog procedures must return a value each time
they are called. The valid return values depend on the message, but
are almost always zero (0) if you want the default action to occur
or one (1) if you do not want the default (i.e. you handled the message
yourself). Note: WM_INITDIALOG is different! Read on...
You usually want to perform some actions as soon as the dialog and
controls exist, but before they are made visible. These are actions
such as positioning database files and loading default values into input
fields (edit controls). You do this by using the ON INIT clause of SHOW
DIALOG, or by handling the WM_INITDIALOG message, or by using the
OnInitDialog() method of class WDialog.
If you handle WM_INITDIALOG, you should return 1 unless you set the
input focus - in which case return 0. A return value of 1 means that
Windows will set the input focus to the first control it can that has
the WS_TABSTOP setting. SHOW DIALOG and method WDialog:OnInitDialog()
both return 1 by default.
After WM_INITDIALOG, quite a number of different messages can be sent
during the life of a dialog, but the only type you're likely to need are
WM_COMMAND messages, as explained below.
In many cases, the only processing you need to do during the user's
interaction with a dialog can be carried out by VALID blocks and/or by
handling the requests to end the dialog. For more information see the
"Validation" section, which discusses WHENs and such things as using a
status message area.
In Windows, your users are supposed to use the Tab key to move between
input fields. The Enter key is meant to signal that the dialog is
complete.
However, if your users want to use Enter to move between input fields
(e.g. to do fast data entry), you can add extra code to allow it. All
you have to do is to check whether the current control is the OK button
(or maybe the last input field on the form). If not, use SetFocus() to
move the input focus to the relevant input field. For an example, see
SOURCE\CONTRIB\ENTER.ZIP or SOURCE\LOGON.PRG.
By default, dialogs are ended when the user selects Close (if you use
the dialog style WS_SYSMENU) or presses Enter, Escape or Alt+F4.
Your application can decide not to close a dialog, e.g. because the
form is not complete, or your application can carry out some work of
its own before closing a dialog. To do this you handle the WM_COMMAND
message and the ID values you're interested in (as stated above, the ID
is in the nwParam). For Enter, the ID is usually IDOK. For a cancel
of any kind you usually want IDCANCEL.
Clip-4-Win's "@ ID ... BUTTON" UDC makes it very easy to handle these
WM_COMMAND messages, because it arranges to call a function you name
if the message is sent.
E.g.
@ ID IDOK BUTTON MyOkFunc() TEXT "&Ok" AT 90,5 SIZE 37,12
@ ID IDCANCEL BUTTON CancelFn() TEXT "&Cancel" AT 90,19 SIZE 37,12
the above buttons result in function MyOkFunc() or CancelFn() being
called if the user presses a button. The appropriate function is also
called if the user tries to end the dialog.
If a user tries to end a dialog or presses Enter, IDOK/IDCANCEL are
nearly always sent. About the only way to prevent them ever being sent
is to have buttons with the appropriate ID's and disable the buttons
using EnableWindow(). If you don't want your user to see the buttons,
either hide them using ShowWindow() with SW_HIDE or put the buttons
outside the visible area of the dialog.
See SOURCE\LOGON.PRG for a sample. It also allows movement between
input fields using the Enter key, as does SOURCE\CONTRIB\ENTER.ZIP.
If the default action is not appropriate, you can readily handle these
WM_COMMAND messages yourself, no matter which programming style you like
to use. For example, if you like UDC's you can use @ ID...BUTTON, which
allows you to specify a function to be called if that button is pressed.
You can of course use your own dialog procedure and check for the
WM_COMMAND message signifying the button press. In addition, the WDialog
class not only has the OnCommand() method, corresponding to WM_COMMAND,
but also the methods Ok() and Cancel(). The default WDialog:OnCommand()
automatically evaluates any code block associated with the buttons, or
else calls the Ok()/Cancel() method. Consequently, you usually provide
a code block for the button or subclass WDialog and override the Ok()
and/or Cancel() method. For a button whose ID is not IDOK or IDCANCEL,
you usually specify a code block or override OnCommand().
If you prefer to have a Save button, that's no problem. You can still
use IDOK for it (but you don't have to do so).
Converting Forms - An Example
-----------------------------
Here's a simple DOS screen:
LOCAL cZip, ; // Zip code variable
cState // State code variable
. . .
SCROLL()
cZip := SPACE(5)
cState := " "
@ 10,0 SAY "Enter zip code" GET cZip PICTURE "@!"
READ
IF LastKey() == 27
EXIT
ENDIF
@ 12,0 SAY "State that zip code resides is " + Zip2St(cZip)
@ 14,0 SAY "Now enter state code" GET cState PICTURE "!!" ;
VALID IsState(cState)
READ
IF LastKey() == 27
EXIT
ENDIF
. . .
The closest Clip-4-Win equivalent is probably the heavily UDC-based
version below. It doesn't use resources, so originally deciding the
positions of the controls could have been quite painful. Also, since
it doesn't use resources, it takes a while to build the dialog. Lastly,
it's using objects, which slows it down some more. These are small
overheads on all but the slowest of systems, but maybe your clients
have slow systems - or maybe you'd rather avoid UDC's and/or objects.
If so, further examples follow.
#include "windows.ch"
#include "commands.ch"
. . .
cZip := SPACE(5)
cState := " "
CREATE DIALOG oDlg TITLE "ZIP" AT 50,50 SIZE 100,40 IN oWnd
@ ID 100 SAY "Enter zip code" AT 10,12 SIZE 50,14 IN oDlg
@ ID 101 GET cZip PICTURE "@!" AT 60,10 SIZE 24,14 IN oDlg
SHOW DIALOG oDlg RESULT nRet
IF nRet == IDCANCEL
EXIT
ENDIF
CREATE DIALOG oDlg TITLE "State" AT 50,50 SIZE 130,60 IN oWnd
@ ID 100 SAY "State that zip code resides is " + Zip2St(cZip) ;
AT 10,10 SIZE 110,14 IN oDlg
@ ID 101 SAY "Now enter state code" AT 10,32 SIZE 80,14 IN oDlg
@ ID 102 GET cState PICTURE "!!" AT 90,30 SIZE 18,14 IN oDlg ;
VALID IsState(cState)
SHOW DIALOG oDlg RESULT nRet
IF nRet == IDCANCEL
EXIT
ENDIF
. . .
Next, let's try the same thing without objects. Using Clip-4-Win's
UDC's it's quite similar:
#include "windows.ch"
#include "dialog.ch"
#define NO_C4WCLASS
#include "commands.ch"
STATIC cZip, ; // Zip code variable
cState // State code variable
. . .
SET SCOREBOARD OFF
SET CONFIRM ON
. . .
LOCAL GetList, aDlg, nRet
. . .
cZip := SPACE(5)
cState := " "
GetList := {}
CREATE DIALOG aDlg TITLE "ZIP" AT 50,50 SIZE 100,40
@ ID 100 SAY "Enter zip code" AT 10,12 SIZE 50,14
@ ID 101 EDIT AT 60,10 SIZE 24,14
SHOW DIALOG aDlg RESULT nRet ;
ON INIT {|hDlg| ZipDlg(hDlg, GetList)}
IF nRet == IDCANCEL
EXIT
ENDIF
GetList := {}
CREATE DIALOG aDlg TITLE "State" AT 50,50 SIZE 130,60
@ ID 100 SAY "State that zip code resides is " + Zip2St(cZip) ;
AT 10,10 SIZE 110,14
@ ID 101 SAY "Now enter state code" AT 10,32 SIZE 80,14
@ ID 102 EDIT AT 90,30 SIZE 18,14
SHOW DIALOG aDlg RESULT nRet ;
ON INIT {|hDlg| StateDlg(hDlg, GetList)}
IF nRet == IDCANCEL
EXIT
ENDIF
. . .
STATIC FUNCTION ZipDlg(hDlg, GetList)
@ DIALOG hDlg ID 101 GET cZip PICTURE "@!"
RETURN NIL
STATIC FUNCTION StateDlg(hDlg, GetList)
@ DIALOG hDlg ID 102 GET cState PICTURE "!!" VALID IsState(cState)
RETURN NIL
The main changes are that GetList and two small functions have been
added. These functions are so that the edit controls can be used as
GETs. This was happening "behind the scenes" in the version using
objects (in the WDialog and WGet classes).
Incidentally, the above example makes use of a small convenience:
"@ ... ID" defaults to "IN aDlg" (or "IN self" for an object oriented
program).
Let's explore the problem some more, and consider using resources.
It's a simple example, but has a common requirement: one of the fields
is only known at runtime. That means the resources must be modified
at runtime.
Here's the version using resources and UDC's:
#include "windows.ch"
#include "dialog.ch"
#define NO_C4WCLASS
#include "commands.ch"
STATIC cZip, ; // Zip code variable
cState // State code variable
. . .
SET SCOREBOARD OFF
SET CONFIRM ON
. . .
LOCAL GetList, aDlg, nRet
. . .
cZip := SPACE(5)
cState := " "
GetList := {}
CREATE DIALOG aDlg RESOURCE "ZIP"
SHOW DIALOG aDlg RESULT nRet ;
ON INIT {|hDlg| ZipDlg(hDlg, GetList)}
IF nRet == IDCANCEL
EXIT
ENDIF
GetList := {}
CREATE DIALOG aDlg RESOURCE "State"
SHOW DIALOG aDlg RESULT nRet ;
ON INIT {|hDlg| StateDlg(hDlg, GetList)}
IF nRet == IDCANCEL
EXIT
ENDIF
. . .
STATIC FUNCTION ZipDlg(hDlg, GetList)
@ DIALOG hDlg ID 101 GET cZip PICTURE "@!"
RETURN NIL
STATIC FUNCTION StateDlg(hDlg, GetList)
SetDlgItemText(hDlg, 100, "State that zip code resides is " + Zip2St(cZip))
@ DIALOG hDlg ID 102 GET cState PICTURE "!!" VALID IsState(cState)
RETURN NIL
Things to notice: various @ SAY's have disappeared (they're in the
resource file); cZip and cState are STATIC; and we modify the text of
a static text control using SetDlgItemText(). The control's ID (100)
better be the same in both the Clipper code and the resource file!
You can do this easily, because from a resource editor you can get an
#include file with #defines suitable for Clipper.
The resource file with the two dialogs (zip and state) follows. Bear
in mind that you don't have to edit it yourself: that's what a resource
editor does.
However, for the curious, here are some details: groups of four numbers
are left, top, width, height (in that order); a fifth number preceding
a group of four numbers is the ID; vertical bar ("|") means binary OR
(think of it as "+" in Clipper); LTEXT is left-aligned text; EDITTEXT
is an edit control (input field). If you really want to know more, try
experimenting with a resource editor and/or consult some of the sources
of information about Windows (such as MSDN or the Windows SDK).
#include "windows.ch"
zip DIALOG 50,50,100,40
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION
CAPTION "ZIP"
BEGIN
LTEXT "Enter zip code", 100, 10,12,50,14
EDITTEXT 101, 60,10,24,14
END
state DIALOG 50,50,130,60
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION
CAPTION "State"
BEGIN
LTEXT "", 100, 10,10,110,14
LTEXT "Now enter state code", 101, 10,32,80,14
EDITTEXT 102, 90,30,18,14
END
Finally, for those who want to program much closer to Windows, here's
a version for you, where the processing of OK and Cancel can be clearly
seen. As this version is avoiding UDCs, it does not need the #include
"commands.ch" used in the other examples.
#define WIN_WANT_ALL
#include "windows.ch"
#include "dialog.ch"
. . .
SET SCOREBOARD OFF
SET CONFIRM ON
. . .
cZip := SPACE(5)
cState := " "
GetList := {}
nRet := DialogBox( , "ZIP", , ;
{|hDlg, nMsg, nwParam, nlParam| ;
ZipDlg(GetList, hDlg, nMsg, nwParam, nlParam)})
IF nRet == IDCANCEL
EXIT
ENDIF
GetList := {}
nRet := DialogBox( , "state", , ;
{|hDlg, nMsg, nwParam, nlParam| ;
StateDlg(GetList, hDlg, nMsg, nwParam, nlParam)})
IF nRet == IDCANCEL
EXIT
ENDIF
. . .
STATIC FUNCTION ZipDlg(GetList, hDlg, nMsg, nwParam, nlParam)
DO CASE
CASE nMsg == WM_INITDIALOG
@ DIALOG hDlg ID 101 GET cZip PICTURE "@!"
RETURN 1
CASE nMsg == WM_COMMAND
DO CASE
CASE nwParam == IDOK
IF IsDialogOK(hDlg, nwParam)
EndDialog(hDlg, nwParam)
RETURN 1
ENDIF
CASE nwParam == IDCANCEL
CANCEL DIALOG hDlg
EndDialog(hDlg, nwParam)
RETURN 1
ENDCASE
ENDCASE
RETURN 0
STATIC FUNCTION StateDlg(GetList, hDlg, nMsg, nwParam, nlParam)
DO CASE
CASE nMsg == WM_INITDIALOG
SetDlgItemText(hDlg, 100, "State that zip code resides is " + Zip2St(cZip))
@ DIALOG hDlg ID 102 GET cState PICTURE "!!" VALID IsState(cState)
RETURN 1
CASE nMsg == WM_COMMAND
DO CASE
CASE nwParam == IDOK
IF IsDialogOK(hDlg, nwParam)
EndDialog(hDlg, nwParam)
RETURN 1
ENDIF
CASE nwParam == IDCANCEL
CANCEL DIALOG hDlg
EndDialog(hDlg, nwParam)
RETURN 1
ENDCASE
ENDCASE
RETURN 0
Validation
----------
Windows introduces some new complications with input validation as well
as the ability for your user to use the mouse to move around your form
in any order.
When an edit control (or any window) is given the input focus it is
sent a WM_SETFOCUS message, and when it loses the focus it receives a
WM_KILLFOCUS message. In general, no message is sent to ask whether
the control is willing to receive or lose the focus. Focus can be
going to a window/control in the current application or a different
application.
Clip-4-Win evaluates any WHEN block when a WM_SETFOCUS arrives, and
it evaluates any VALID block when a WM_KILLFOCUS arrives. This is
very close to DOS Clipper's behaviour.
Bear in mind that your user can move around using the mouse and may be
able to signal a form is complete at any time. Consequently, it's a
good idea to do relatively little validation on each input, and do the
rest only when/if the user does signal the form is (supposedly) complete.
You can enable/disable controls at any time, using EnableWindow(), and
this may well help both you and your users. For example, a disabled
Save button will appear grayed and cannot be pressed.
It's much better to disable a control instead of using a WHEN that
prevents the user accessing the control. This is because a user can
see that a control is disabled, but gets no visual feedback from a
control with a WHEN until trying to move to it - only to find it's not
allowed. If you use a status message area, you might like to use WHENs
that set the status message and then return TRUE.
One thing you might do in a VALID which fails validation, is to prompt
your user in some way. This is likely to cause further focus changes.
Similarly, using ALERT() or MessageBox() is going to cause more focus
changes, so more messages will be sent. You can easily cause a "storm"
of messages which either confuse you (or your code), or cause the stack
to overflow.
If you want to know the messages that are being sent to a dialog, avoid
anything that causes further messages, i.e. avoid ALERT and MessageBox().
Instead, write the messages out to a file, or another window, or to a
status area, or even use SetWindowText() to change a window's caption.
SOURCE\DBG.PRG logs messages to an array which can be examined at
any later time. A debug window is provided by SOURCE\DBWIN2.ZIP and
can be accessed using the OutputDebugString() Windows API function
(in SOURCE\WINAPI\OUTDEBUG.PRG) and the UDC's DbgTrace(), $ and $$
defined by INCLUDE\WINDOWS.CH if you compile with /dWIN_DBWIN
(or use #define WIN_DBWIN).
Note: The Windows API function MessageBox() does not always restore the
focus properly, so if you use MessageBox() you should program around
this. E.g.
LOCAL hFocus
. . .
hFocus := GetFocus()
nRet := MessageBox(...)
SetFocus(hFocus)
Overall, you should write your VALIDs to be able to be evaluated more
than once and to be cautious about focus changes.
Here's part of a DOS form (see SOURCE\FORMDOS.PRG):
mName := SPACE(35)
mFTName := SPACE(35)
mAddr_1 := SPACE(35)
mAddr_2 := SPACE(20)
mAddr_3 := SPACE(20)
mAddr_4 := SPACE(15)
mAddr_5 := SPACE(10)
mContact := SPACE(25)
mTelephone := SPACE(15)
mFax := SPACE(15)
DO WHILE .T.
CLEAR GETS
CLS
@ 3,55 SAY "[ Add Contact ]"
@ 5,13 SAY "Name : " GET mName PICTURE '@!' valid genval(!empty(mName),"Need a Name Please")
@ 6,13 SAY "Address 1 : " GET mAddr_1 PICTURE '@!' valid genval(!empty(mAddr_1),"Need an Address Please")
@ 7,13 SAY "Address 2 : " GET mAddr_2 PICTURE '@!' valid genval(!empty(mAddr_2),"Need an Address Please")
@ 8,13 SAY "Address 3 : " GET mAddr_3 PICTURE '@!' valid genval(!empty(mAddr_3),"Need an Address Please")
@ 9,13 SAY "Address 4 : " GET mAddr_4 PICTURE '@!'
@ 10,13 SAY "Post Code : " GET mAddr_5 PICTURE '@!'
@ 11,13 SAY "Contact : " GET mContact PICTURE '@!' valid genval(!empty(mContact),"Need a Contact Please")
@ 12,13 SAY "Telephone : " GET mTelephone PICTURE '@!' valid genval(!empty(mTelephone),"Need a Telephone Number Please")
@ 13,13 SAY "Fax No : " GET mFax PICTURE '@!'
@ 14,13 SAY "FT Name : " GET mFTName PICTURE '@!' valid genval(!empty(mFTName),"Need a Name for Financial Transactions Please")
READ
if lastkey() == 27
EXIT
ENDIF
. . . update . . .
ENDDO
. . .
static function genval(lOk, cMsg)
if !lOk
@ 24,10 CLEAR TO 24,70
@ 24,10 SAY cMsg
tone(523.3,0.5)
endif
return lOk
Below is a close translation of the main loop and some validation using
Clip-4-Win. Note how the code in function OnOk() is simplified by
using consecutively numbered edit control ID's. Also, note how easily
you can use a status area placed at the bottom of the dialog.
(See SOURCE\FORMWIN.PRG).
. . . #includes, #define NO_C4WCLASS, SET etc. as before . . .
LOCAL GetList, aDlg, nRet
. . .
DO WHILE .T.
CLEAR GETS
CREATE DIALOG aDlg TITLE "Add Contact" AT 50,10 SIZE 220,160
@ ID 100 SAY "Name: " AT 10,12 SIZE 45,10
@ ID 210 EDIT AT 48,10 SIZE 105,14
@ ID 101 SAY "Address 1: " AT 10,28 SIZE 45,10
@ ID 211 EDIT AT 48,26 SIZE 105,14
@ ID 102 SAY "Address 2: " AT 10,44 SIZE 45,10
@ ID 212 EDIT AT 48,42 SIZE 60,14
@ ID 103 SAY "Address 3: " AT 10,60 SIZE 45,10
@ ID 213 EDIT AT 48,58 SIZE 60,14
@ ID 104 SAY "Address 4: " AT 10,76 SIZE 45,10
@ ID 214 EDIT AT 48,74 SIZE 45,14
@ ID 105 SAY "Post Code: " AT 120,76 SIZE 35,10
@ ID 215 EDIT AT 158,74 SIZE 35,14
@ ID 106 SAY "Contact: " AT 10,92 SIZE 45,10
@ ID 216 EDIT AT 48,90 SIZE 75,14
@ ID 107 SAY "Telephone: " AT 10,108 SIZE 45,10
@ ID 217 EDIT AT 48,106 SIZE 45,14
@ ID 108 SAY "Fax No: " AT 120,108 SIZE 35,10
@ ID 218 EDIT AT 158,106 SIZE 45,14
@ ID 109 SAY "FT Name: " AT 10,124 SIZE 45,10
@ ID 219 EDIT AT 48,122 SIZE 105,14
// here's a status (message) area:
@ ID 300 SUNKENFRAME AT 3,145 SIZE 214,13
@ ID 301 SAY "" AT 5,147 SIZE 190,10
// dialogs behave as if this button always exists:
// (let's use it to do extra validation)
@ ID IDOK BUTTON OnOk(GetList)
SHOW DIALOG aDlg RESULT nRet ON INIT {|hDlg| FormInit(hDlg, GetList)}
if nRet == IDCANCEL
EXIT
ENDIF
. . . update . . .
ENDDO
. . .
static function FormInit(hDlg, GetList)
@ DIALOG hDlg ID 210 GET mName PICTURE '@!' valid genval(!empty(mName),"Need a Name Please", hDlg)
@ DIALOG hDlg ID 211 GET mAddr_1 PICTURE '@!' valid genval(!empty(mAddr_1),"Need an Address Please", hDlg)
@ DIALOG hDlg ID 212 GET mAddr_2 PICTURE '@!' valid genval(!empty(mAddr_2),"Need an Address Please", hDlg)
@ DIALOG hDlg ID 213 GET mAddr_3 PICTURE '@!' valid genval(!empty(mAddr_3),"Need an Address Please", hDlg)
@ DIALOG hDlg ID 214 GET mAddr_4 PICTURE '@!'
@ DIALOG hDlg ID 215 GET mAddr_5 PICTURE '@!'
@ DIALOG hDlg ID 216 GET mContact PICTURE '@!' valid genval(!empty(mContact),"Need a Contact Please", hDlg)
@ DIALOG hDlg ID 217 GET mTelephone PICTURE '@!' valid genval(!empty(mTelephone),"Need a Telephone Number Please", hDlg)
@ DIALOG hDlg ID 218 GET mFax PICTURE '@!'
@ DIALOG hDlg ID 219 GET mFTName PICTURE '@!' valid genval(!empty(mFTName),"Need a Name for Financial Transactions Please", hDlg)
return nil
static function OnOk(GetList, hDlg)
local aRequired := {.t., .t., .t., .t., .f., .f., .t., .t., .f., .t.}
local i, n := len(GetList), oGet
// check all required fields have been entered
for i = 1 to n
oGet = GetList[i]
if !oGet:BadDate() .and. oGet:Changed
oGet:Assign()
endif
if aRequired[i]
if empty(oGet:VarGet())
// force input focus to this one!
// Note: the ID's count from 210...
SetFocus(GetDlgItem(hDlg, 210 + i - 1))
exit
endif
endif
next i
return nil
static function genval(lOk, cMsg, hDlg)
SetDlgItemText(hDlg, 301, "")
if !lOk
SetDlgItemText(hDlg, 301, cMsg)
tone(523.3,0.5)
endif
return lOk
Converting Custom GET Readers
-----------------------------
Custom GET readers need converting to appropriate things in Windows.
These include check boxes, radio buttons, and list boxes / browsers.
If necessary, really unusual controls can be implemented, e.g. using
owner-drawn or subclassed controls, but the consistent look and feel
encouraged by Windows means you should avoid creating special controls.
Many controls send notification messages as the user interacts with
them, and these provide another chance for special requirements to be
implemented. Notification messages are WM_COMMAND messages with a
notification code in C4W_HiWord(nlParam). For example, edit controls
send WM_COMMAND with the EN_* values (defined in WINDOWS.CH), and
list boxes send WM_COMMAND with LBN_* notification codes.
WBROWSE gives you various options by providing a DoubleClick block,
a FindBlock, a KeyBlock, aAltKeys, and even a MsgBlock.
You can do almost anything if you really must, but it's so very unusual
that you'll have to refer to documentation about Windows, as listed
previously. Look for messages such as WM_KEYDOWN and WM_SYSKEYDOWN.
You might also want to handle WM_KEYUP, but that's even rarer because
it does not occur during auto-repeating keys (keys held down).
Converting Data Driven Applications
-----------------------------------
Data driven code can usually be converted fairly easily because using
Clip-4-Win you can readily build/modify menus, dialogs, etc. at runtime.
You should probably re-structure your menus, largely in line with the
section "Converting Menus" - except you'll probably be loading at least
parts of your menus from data files.
The discussions of Data Entry Forms and Validation apply to data driven.
Whether it's easy to convert your code (and the data that drives it) can
depend heavily on how easily you can keep or adapt your validation logic.
You will probably have to change your data to add information specific
to Windows e.g. control types (push button, check box, etc.).
If you want to build dialogs at runtime, you probably have to add more
precise positioning information than row and column.
The above changes are important but may not be terribly hard. If you
wish, you can use your new driver file(s) with your DOS Clipper code.
A small aside: if you use any INI files, keep them small and don't use
them too often. They're limited to 64KB and tend to be quite slow.
Converting Printing/Reports
---------------------------
For reports, there are some really good report writers available for
Windows, such as R & R for Windows and Crystal Reports.
For interface code for R & R, see SOURCE\RRW*.PRG.
For samples using Crystal Reports see SOURCE\CONTRIB\CRYSTAL.ZIP,
SOURCE\CONTRIB\CRWCLS.PRG and SOURCE\ADDMGR\CRW.PRG. (Some of these
are not shipped with the Evaluation version of Clip-4-Win.)
For Report Smith, see SOURCE\RPTSMTH.PRG.
For CA-RET, see SOURCE\CONTRIB\CARET.ZIP and SOURCE\OO\CMD\CARET.PRG.
CA-Visual Express is similar.
For other printing, you may still be able to print directly to the printer
as with DOS Clipper, but Windows does not encourage it and in any case you
may want to stop using all those ESCape sequences. Taking advantage of
the device independence provided by Windows means you can use almost all
the same code to output to the screen or to any printer.
When converting an application, you are recommended to get your user
interface and related changes working, and do printing later. This means
you will have gained useful experience, and it may even help save a tree
or two!
Expect printing under Windows to be slower than under DOS. There are
several reasons, including spooling via Print Manager, font mapping
(which you may not want), the device independence provided by Windows,
and the much more complex device drivers involved.
About fonts and font mapping: Windows tries to match your application's
requested fonts with those available on the device, and substitutes if
it thinks it's necessary. This is fine when it works, but a real pain
when it doesn't. Don't contact us about it, please, because we don't
know the answer!
If you want to print bar codes, you can buy fonts to do it. Ask your
software supplier for their recommendations. You can also get Windows
add-ons that provide bar code support using a DLL, which you should be
able to use via Clip-4-Win's _DLL UDC or CallDLL(), which is used by
_DLL.
Some samples showing simple printing include SOURCE\PRINTHEL.PRG,
SOURCE\PRINTER.PRG, SOURCE\PRINTFLS.PRG, SOURCE\PRINTDC.ZIP and
SOURCE\WBTDEMO.ZIP. Please spend some time looking at them when you
want to know about printing.
Without using SET PRINTER TO, here's about the shortest way to print
"Hello World!" on the default printer. You can find this and other
samples in SOURCE\PRINTHEL.PRG.
The actual printing is done using a "device context". You can think
of a device context as being a little like a work area. It's something
you have to use to do certain things (in this case output), and it has
its own set of functions that must be used. These functions can both
control and query the device.
As you should expect in Windows, you deal with a device context using
a handle.
You can get a handle to a printer device context using functions such
as GetPrintDC() and PrintDlg(). When you've finished printing you
*must* delete the handle using DeleteDC(), otherwise you lose important
internal Windows memory (the limited part that stores resources when
in memory).
WARNING: Freeing resources is vital! Going back to the comparison
with a work area, failing to free resources is much worse
than failing to close your work areas. Windows is likely
to crash eventually if you don't free things properly!
Always free things as soon as you can. That makes it less
likely you'll forget, and also means Windows can share its
limited resources with other apps without running out.
An example that queries the device is to get the size of a page:
nWidth := GetDeviceCaps(hDC, HORZRES)
nHeight := GetDeviceCaps(hDC, VERTRES)
static function DoHello()
local hDC
if !empty(hDC := GetPrintDC()) // see also: PrintDlg()
StartDoc(hDC, "SomeDocument")
StartPage(hDC)
TextOut(hDC, 50, 50, "Clip-4-Win says: Hello World!")
EndPage(hDC)
EndDoc(hDC)
DeleteDC(hDC)
endif
return nil
Here's the same thing, but with some extra printing in Arial 10 point
and also a bitmap.
Note how easily you can create a font; see FONT.CH for more information.
As usual in Windows, what you get is a handle to the item - in this case
a font handle. When you've finished with it, you *must* delete it using
DeleteObject(), otherwise you'll lose Windows resources.
Actually using the font is just a little work: you use SelectObject().
It's a nuisance, but you must save and restore the old font. If you
don't, you'll again lose Windows resources.
Please re-read the WARNING above, about freeing resources!
#include "font.ch" // for CREATE FONT
static function DoArial()
local hDC, hFont, hOldFont, cBmp
if !empty(hDC := GetPrintDC()) // see also: PrintDlg()
CREATE FONT HANDLE hFont NAME "Arial" SIZE 10 DC hDC
StartDoc(hDC, "SomeDocument")
StartPage(hDC)
TextOut(hDC, 50, 50, "Clip-4-Win says: Hello World!")
hOldFont = SelectObject(hDC, hFont)
TextOut(hDC, 50, 150, "In Arial 10: Hello World!")
SelectObject(hDC, hOldFont)
cBmp = ReadDIB(hDC, "CLIP4WIN.BMP")
ShowDIB(hDC, cBmp, 200, 250)
EndPage(hDC)
EndDoc(hDC)
DeleteObject(hFont)
DeleteDC(hDC)
endif
return nil
Converting From Modal To Modeless
---------------------------------
Most DOS Clipper applications are modal: in each screen your user has
a limited number of choices available, e.g. menus can only be accessed
if no data entry form is currently active. In a completely modeless
application, your user could do anything at any time.
Modal applications/screens restrict users, and are usually easier to
develop. Windows encourages less modality, especially when browsing
data (including previews).
Of course, business applications have more places where modal behaviour
is sensible than word processors, drawing programs, etc. File/record
locks are too important to be kept indefinitely, so you have to limit
modeless screens and/or release locks after a reasonable time interval.
If you convert your DOS application in a straightforward way, you'll
get a mainly modal Windows application, although browsers and previews
tend to be modeless unless you intervene. The only other exception may
be your menus. If you find that these allow unwanted activities to be
started, you will have to disable some menu items from time to time.
This kind of straightforward application is often called SDI (Single
Document Interface; don't dwell on the word "Document"). The other end
of the spectrum is MDI (Multiple Document Interface), where you have a
main ("frame" or "shell") window and any other ("child") windows are
inside the frame, the way Program Manager is organised.
Doing MDI properly means extra work: as many child windows as sensible
should be modeless (non-modal), your last two popup menus should be
"Window" (with specific menu items e.g. using the UDC MDIWINDOWPOPUP)
and "Help", any child menus should be displayed in the frame, and so
on. To learn more, use some of the most successful Windows applications
and check out as many sources of Windows information as you can. The
MS documentation is rather incomplete in some areas, but still good in
other areas. See "Windows Interface Guidelines for Software Design",
listed previously.
It's probably not a good idea to do MDI if you're a beginner in Windows
or Clip-4-Win. Also, unless you're adventurous, plan to use objects to
handle the standard stuff just listed. The WMDIApp, WDialog, WMDIFrame,
WMDIClient and WMDIChild classes are all organised to co-operate in
making MDI reasonably painless. The WTable class provides encapsulated
access to databases/work areas. You can't use STATICs for data entry
(unless you force the data entry form to be used only once at a time,
which isn't ideal for MDI) - instead put them in an array or object,
e.g. as done by SOURCE\OO\USERSRES\USERSMDI.PRG.
Now that you know some of the pitfalls of MDI, why does anyone use it?
Mainly to provide increased convenience for users, and to be consistent
with the leading Windows applications. You can do the same, if you're
willing to expend the extra effort required.
It's worth pointing out that you can have modeless activities in an SDI
application, and you can have modal ones in an MDI application. This
sounds like there's some sort of overlap between the two, and although
that's inevitably somewhat so, you should do your very best to make
your mind up sooner rather than later. As with other software issues,
changing your strategy is possible but means more work.
Despite the above, converting a well-written OO application from SDI to
MDI tends to be fairly easy, because using classes should mean everything
is well encapsulated.
If you're really not sure whether you should use MDI, you should almost
certainly go for SDI.
Let's suppose you start with SDI, e.g. because it's less work and more
like the structure of your DOS application.
Next, suppose you want fewer modal screens but without necessarily going
to MDI.
For browsers, you need to create a new window for each browse - as you
may already be doing. You have to decide whether to let your user open
browsers in such a way that you need a database in two or more places
at the same time. If you do, you can either save and restore all the
information about position etc. or you can use the WTable class to open
the file multiple times, each in its own work area and with a unique
alias. (Or you could use a Clipper add-on such as dbClass or ObjectDB.)
If you decide to write your own code (instead of using WTable etc.) to
allow the same database to be used in two or more places concurrently
without opening the database more than once, you have to save and
restore all the work area information (and any connected information).
This is so painful that you should probably think again! However, if
you decide to continue, when do you save and restore? The answer
depends on the way you've written your application, but is likely to
be whenever you start or end a screen (form or browse), and any time
a browse needs data or movement occurs.
If your browsers never lock any files or records, and never add, edit or
delete any records, your work is nearly done. You just have to get any
active browse to refresh its output if your user adds, edits or deletes
a record somewhere else in your application. You can use the WTable
class to help with this, as it has a ReDo() method specifically for a
situation like this.
If, as is common, your user can add, edit or delete when browsing, and
you do this in a modal way, you're in the same situation as above.
Otherwise, you'll have to think about, and probably enhance, your
strategies for locking files and records. You're in danger of letting
your user lock the same record or file more than once, from a single
application. Yes, it's potentially worse than multi-user (network)
programming.
Several of the samples in SOURCE\OO\USERS and SOURCE\OO\USERSRES use
WTable:ReDo(). They're dialogs; a browser is easier: the ReDo() method
just needs to oBrowse:RefreshAll().
Let's continue with this SDI application we're trying to make less
modal.
Dialogs can typically be converted to be modeless with a trivial change
to the source code, e.g. by adding the MODELESS clause to SHOW DIALOG
or by changing DialogBox() to CreateDialog(), but your application will
then usually malfunction!
Why? Because your code that puts the dialog on the screen will expect
the dialog to have been completed or cancelled before it returns. A
modeless dialog, however, starts up but then returns and at the same
time carries on running! The equivalent in DOS Clipper would be for
ACHOICE(), MEMOEDIT(), or READ/READMODAL() to start their work and then
return without waiting for the user - and yet also keep the screen
showing their output and the user able to interact with it as usual.
If you're having trouble imagining the mayhem that would result, try
looking at some of your DOS Clipper code. Find an ACHOICE(), MEMOEDIT()
or READ/READMODAL() and look at what the code just after it does. It
probably uses the result(s) or LASTKEY() in some way. In a modeless
situation, the results are not yet available and no key has yet been
pressed.
Now imagine you could put the code that needs the result(s) and/or
LASTKEY() somewhere else, so that it only gets run at the right time.
This is exactly what's needed for event driven programming.
For a dialog, instead of immediately after the code that creates the
dialog, you put your result-handling code where it will be run when the
user tries to end or cancel the dialog (or presses a Save button, if you
use that mechanism). So, not only do you perform any extra validation
needed when the user presses OK/Save, you also do updates etc. at the
same time. This is the way all the versions of the USERS sample work,
even the modal ones, largely because this way you can change between
modal and modeless very easily.
The USERS samples are discussed in the tutorials (in the NG, HLP and
manuals). The source code is in SOURCE\USERS, SOURCE\USERSRES,
SOURCE\OO\USERS, and SOURCE\OO\USERSRES.
Converting: Alphabetical Clipper Commands/Functions
---------------------------------------------------
? / ??
For testing, usually works. Needs removing eventually, or converting
to e.g. a MessageBox().
@...BOX
Not usually appropriate in Windows. Often indicates a new menu or
form, in which case convert as appropriate. In a form, a Group Box
is the control you want. If your code really is drawing, try
MoveTo(), LineTo(), PolyLine(), Polygon(), Rectangle(), etc. (see
"drawing and output functions" in the NG/HLP).
@...CLEAR
Not appropriate in Windows. Often indicates a new menu or form,
in which case convert as appropriate.
@...GET
Almost always need converting to a control (usually an edit control)
in a dialog. See @ ID ... EDIT, @ ID ... GET, @ DIALOG ... GET.
Be wary of trying to control the input focus e.g. during a VALID;
you may cause an error or annoy your user.
@...PROMPT
Use Clip-4-Win's menus (or maybe use push buttons).
@...SAY
Almost always need converting to use a static text control in a
dialog. Despite its name, you can change the contents of a static
text control, e.g. using SetWindowtext(), and some of its versions
aren't text at all, e.g. SS_ICON.
ACCEPT
For testing, usually works. Needs removing eventually, or converting
to e.g. a dialog.
ACHOICE()
Supported by Clip-4-Win's ACHOICE.PRG.
ALERT()
Supported by Clip-4-Win's ALERT.PRG.
ALTD()
Only supported with no parameters; invokes the debugger.
BROWSE()
Look at WBrowseDef() in SOURCE\WBROWDEF.PRG, which uses WBROWSE
(see HLP\WBROWSET.HLP).
CHECKBOX()
Use a CHECKBOX control, almost always in a dialog.
CLEAR GETS
To terminate a modal dialog, use EndDialog(); for a modeless dialog
use DestroyWindow().
CLEAR SCREEN / CLS
For testing, usually works. Needs removing eventually, or converting
to opening a new window e.g. a dialog.
CLEAR TYPEAHEAD
Not appropriate in Windows.
COL()
For testing, usually works. Probably part of a menu or form, in
which case convert the code appropriately.
COLORSELECT()
Not appropriate in Windows.
DBEDIT()
Look at WBrowseDef() in SOURCE\WBROWDEF.PRG, which uses WBROWSE
(see HLP\WBROWSET.HLP and SOURCE\WBTDEMO.ZIP).
DEVOUT() / DEVOUTPICT() / DEVPOS()
Not really appropriate in Windows, but may work adequately.
DISPBEGIN() / DISPCOUNT() / DISPEND()
Not appropriate in Windows.
DISPBOX()
Not appropriate in Windows. Often indicates a new menu or form,
in which case convert as appropriate.
DISPOUT()
May work for testing, but not appropriate in Windows. May be part
of a new menu or form, in which case convert as appropriate.
FKLABEL() / FKMAX()
Not appropriate in Windows.
G (Graphics) Functions
Convert to use Windows functions, e.g. ShowDIB(), Ellipse(),
Rectangle(), MoveTo(), LineTo(), Polygon(), PolyLine(), SetPixel(),
DrawText(), TextOut().
GETNEW()
Convert to a control (usually an edit control) in a dialog. See
@ ID ... EDIT, @ ID ... GET, @ DIALOG ... GET. Be wary of trying
to control the input focus e.g. during a VALID; you may cause an
error or annoy your user.
See Also: "Converting Custom GET Readers".
INKEY()
For testing, usually works - helping you to convert in stages. Needs
removing/converting eventually to e.g. a menu or dialog. If part
of a TBROWSE loop, remove it and if need be set the WBROWSE object's
KeyBlock.
INPUT
For testing, usually works. Needs removing eventually, or converting
to e.g. a dialog.
KEYBOARD
Should not be used in Windows. For some usages, you can get a similar
effect by using PostMessage(), but not for keystrokes.
LASTKEY()
Needs removing/converting. If it's there to handle a menu, it should
be removed (Windows handles the user interface). If it's there to
handle a cancelled form, e.g. checking for ESC, the equivalent for
DialogBox() is to check for the return value IDCANCEL. Similarly,
MessageBox() returns IDOK/IDCANCEL/... depending on the MB_* value(s)
you use for the <nType> parameter (default MB_OK).
LISTBOX()
Use a LISTBOX (or COMBOBOX) control, almost always in a dialog.
MAXCOL() / MAXROW()
For testing, usually works. Probably part of a menu or form, in
which case convert the code appropriately.
M (Mouse) Functions
Not appropriate in Windows. Mouse messages in Windows are handled
through the same central mechanism as keystrokes, timers, window
creation, window destruction, and so on. SetCursor() controls the
kind of mouse cursor.
MEMOEDIT()
Supported by Clip-4-Win's MEMOEDIT.PRG.
MENU TO
Use Clip-4-Win's menus.
MENUITEM()
Use Clip-4-Win's menus.
NEXTKEY()
Not appropriate in Windows.
PCOL() / PROW()
Not really appropriate in Windows, but may work adequately.
POPUP()
Use Clip-4-Win's menus.
PUSHBUTTON()
Use a PUSHBUTTON control, almost always in a dialog.
QOUT() / QQOUT()
For testing, usually works. Needs removing eventually, or converting
to e.g. a MessageBox().
RADIOBUTTO()
Use a RADIOBUTTON control, almost always in a dialog.
RADIOGROUP()
Use a GROUPBOX control, almost always in a dialog.
READ / READMODAL()
Not appropriate in Windows. Probably marks the activation of a form
or menu, in which case convert the preceding code appropriately.
READEXIT()
Not appropriate in Windows.
READFORMAT
Not appropriate in Windows.
READINSERT()
Not appropriate in Windows.
READKEY()
Not appropriate in Windows. See LASTKEY().
READKILL()
Not really appropriate in Windows, but may work adequately.
READUPDATED()
Not really appropriate in Windows, but may work adequately.
READVAR()
Not really appropriate in Windows, but may work adequately with
@ DIALOG ... GET.
RESTSCREEN()
Not appropriate in Windows. Probably marks the end of a menu, popup
window or form, in which case convert the preceding code appropriately.
ROW()
For testing, usually works. Probably part of a menu or form, in
which case convert the code appropriately.
SAVESCREEN()
Not appropriate in Windows. Probably marks the beginning of a menu,
popup window or form, in which case convert the code appropriately.
SCROLL()
For testing, usually works. Probably part of a menu or form, in
which case convert the code appropriately and remove the SCROLL().
SCROLLBAR()
Use a SCROLLBAR control, or use window style WS_HSCROLL and/or
WS_VSCROLL. Avoid scroll bars in dialogs; consider a tabbed dialog
(property sheet page) using the oTab class (see HLP\OTABT.HLP and
SOURCE\OTABDEMO).
SETBLINK()
Not appropriate in Windows.
SETCANCEL()
Not appropriate in Windows.
SET COLOR / SETCOLOR()
Not appropriate in Windows. You can have coloured text in a browser
easily e.g. using WBCOLUMN's bgcolor and fgcolor properties or
WBROWSE's colorSpec. Note that these all use the Windows way of
specifying colours as RGB(nRed, nGreen, nBlue) values.
Coloured text in dialogs is discouraged, but can be done if you
handle the WM_CTLCOLOR message or use an owner-draw control.
SET CONFIRM
You almost certainly want SET CONFIRM ON in your MAIN().
SET CONSOLE
Not really appropriate in Windows, but may work adequately.
SET CURSOR / SETCURSOR()
Different in Windows, where the cursor means the mouse cursor. The
equivalent of the DOS Clipper cursor is known as a caret. You don't
usually alter either the cursor or the caret in the same way as you
do in DOS, so you probably just want to remove your DOS cursor code.
If you know an operation will take quite a while, change the (mouse)
cursor to an hour-glass temporarily:
hCursor := LoadCursor( , IDC_WAIT)
hOldCursor := SetCursor(hCursor)
. . .lengthy activity. . .
SetCursor(hOldCursor) // restore previous cursor
SET DEVICE
Not really appropriate in Windows, but may work adequately.
SET ESCAPE
Supported a little differently in Windows. ESC in a dialog is
generally treated as a Cancel request; you can handle it as you wish.
SET FUNCTION
See SET KEY.
SET INTENSITY
Not appropriate in Windows.
SET KEY / SETKEY()
Hotkeys are called accelerators in Windows. They are easily added
to dialogs by using "&" before a character. Other keys can be
supported using LoadAccelerator(), SetAccelerator(), etc. Apart
from dialogs, try to use them sparingly, and keep them consistent
throughout your application.
SET MARGIN
Not really appropriate in Windows, but may work adequately.
SET MESSAGE
Use Clip-4-Win's menus.
SETMODE()
Not appropriate in Windows.
SETPOS()
For testing, usually works. Probably part of a menu or form, in
which case convert the code appropriately.
SETPRC()
Not really appropriate in Windows, but may work adequately.
SET PRINTER
Not really appropriate in Windows, but may work adequately.
SET SCOREBOARD
You almost certainly want SET SCOREBOARD OFF in your MAIN().
SET TYPEAHEAD
Not appropriate in Windows.
SET VIDEOMODE
Not appropriate in Windows.
SET WRAP
Use Clip-4-Win's menus.
TBCOLUMNNEW()
Use WBCOLUMN{} (see HLP\WBROWSET.HLP and SOURCE\WBTDEMO.ZIP).
If the "{}" cause a compile error, you to need to #include "TopClass.ch"
TBROWSEDB() / TBROWSENEW()
Use WBROWSE{} (see HLP\WBROWSET.HLP and SOURCE\WBTDEMO.ZIP).
If the "{}" cause a compile error, you to need to #include "TopClass.ch"
Remove your "stabilize loop": it's effectively internal to WBROWSE.
TONE()
Appears to work, perhaps surprisingly.
TOPBAR()
Use Clip-4-Win's menus.
UPDATED()
Not really appropriate in Windows, but may work adequately.
WAIT
For testing, usually works. Needs removing eventually, or converting
to e.g. MessageBox().