|
So tell me how
to do more things. How do I manipulate the mouse? The cursor?
The keyboard? Etc.?
There is an object called TSysMouse that lets you get and
set mouse data. And since there is only one mouse on the system,
there is a single instance of this object called SysMouse. Every
EWin program has a SysMouse. So if you want to know how many
buttons the mouse has, you call
|
|
int n = SysMouse.NumBtns(); |
And to find out if the left mouse button is down at a particular
moment, you can call
|
|
bool b = SysMouse.bLeftBtnDown(); |
You can also use SysMouse to move the mouse around programmatically,
or to lock the mouse into a small rectangle of the screen. But
these things are, in general, rude, and should be done only after
careful consideration. In general, you should let the user move
the mouse cursor, and not move it yourself.
(For other mouse-manipulation operations, see the help for
TSysMouse.)
Similarly, there is a TSysKeyboard object, and every EWin
program has an instance of this called SysKeyboard. You use this
to figure out what keys are currently pressed:
|
|
bool bShiftPressed = SysKeyboard.bKeyPressed(VK_SHIFT);
bool bQKeyPressed = SysKeyboard.bKeyPressed('Q');
|
You can also use SysKeyboard to simulate the pressing of keys.
But this is considered rude; in general, you should let the user
press the keys, unless you are adding a macro feature to your
application or something.
There is also a TSysCaret. The caret is the little funkey
thing in an edit box that indicates where text is going to appear
if you type. There can only be one caret in your program at a
time, so there is only one instance of this object. Every EWin
program has a TSysCaret named SysCaret. The caret isn't especially
useful in typical programs, since the caret is maintained for
you. But if you ever need to manipulate the caret, use that object.
There is also a TSysClipboard. This object encapsulates clipboard
operations. Every EWin program has one instance of this object,
named SysClipboard. (There is only one clipboard, so there need
be only one of these objects.) The clipboard can get a bit tricky,
especially for user data types. You will need to look through
the help if you want to do fancy things. But the very simplest
operations, those of getting and setting text and bitmaps from
the clipboard, have been made into utility functions.
To tell if a bitmap is currently on the clipboard, call
|
|
bool bCanGetBitmap = SysClipboard.bBitmapAvailable();
|
Similarly, to see if text is available, call
|
|
bool bCanGetText = SysClipboard.bTextAvailable();
|
For any other type, you will need to know the type code, and
call bFormatAvailable(typecode). But I digress; let me continue
to explain how to get and set text and bitmaps.
To get the bitmap, call GetBitmap(). It returns a TBitmap:
|
|
TBitmap Bmp;
Bmp.Create( SysClipboard.GetBitmap() );
|
You can also get a Palette:
|
|
TPalette Pal;
Pal.Create( SysClipboard.GetPalette() );
|
(And you can see if a palette is available via SysClipboard.bPaletteAvailable().)
With a palette and a bitmap, you can make a DIB, a device independent
bitmap:
|
|
TDib Dib;
Dib.Create( Bmp, Pal );
|
And you can then draw that via Dib.Draw().
Text is similar: See if there is text by calling SysClipboard.bTextAvailable(),
and if so, use SysClipboard.GetText() to get the text from it.
Writing to the clipboard is similar, but requires an extra
step. See, if you just call
|
|
SysClipboard.PutBitmap( Bmp );
|
You will indeed put a bitmap on the clipboard. But now if
you want to also put a palette, you might call
|
|
SysClipboard.PutPalette( Pal );
|
This will put a palette on the clipboard, but the bitmap will
be gone! In order to put more than one data type onto the clipboard
at once, use Open() and Close():
|
|
SysClipboard.Open();
SysClipboard.PutBitmap( Bmp );
SysClipboard.PutPalette( Pal );
SysClipboard.Close();
|
There are a couple more "Sys" objects: TSysInfo
and TSysErrorStrings. There is a single instance of each of these,
named SysInfo and SysErrorStrings, respectively. Use the various
methods of SysInfo to get information about how the system is
set up. It can retrieve user-chosen fonts, user-specified sizes
and settings, etc. For instance, to get the font that Message
Boxes use on this system:
|
|
TFont MBoxFont( SysInfo.MessageBoxFont() );
|
And SysErrorStrings lets you retrieve error strings from the
system. These error strings are typically returned from the GetLastError()
API function, but you can also add your own error strings. Some
add-on libraries, like Mike Kujawa's TSocket networking package,
adds in error strings for sockets. The idea is that SysErrorStrings
is a single repository for system error messages.
So
if I wanted to set the mouse to be a particular cursor, I would
use SysMouse.SetCursor() ?
Yes, but for another reason that I will explain, no. Say you
wanted to set the cursor to be a "wait" cursor, an
hourglass. You could call
|
|
SysMouse.SetCursor( TCursor( scWait ) );
|
And sure enough, the mouse cursor would become an hourglass
--- until the moment the user moved the mouse, at which time
it would revert to its former shape. The reason is that each
window has a mouse cursor associated with it, and whenever the
mouse moves, the operating system sets the mouse cursor to the
shape that the window has requested. And by default, all EWin
windows request that the mouse cursor be the normal pointer shape.
So you need to tell the window in question that the mouse cursor
should be a wait cursor. Easy enough:
|
|
MyWin.Cursor( TCursor( scWait ) );
|
And now, whenever the mouse moves over this window, the cursor
will become a wait cursor.
[MORE] You might think, then, that you need both of these
lines. The first sets the mouse cursor now, and the second sets
it in the future. And this is true. But to try to avoid confusion,
the TWin::Cursor() function calls SysMouse.SetCursor for you.
So you only need to make that call. Of course, calling both functions
won't hurt anything.
Oh, and you can set it back to normal via
|
|
MyWin.Cursor( TCursor( scArrow ) );
|
I
want to store my program's settings in the registry. How do I
do that?
Well, the registry *should* be encapsulated in a TSysRegistry
object. But I haven't gotten around to that yet. Instead, I have
a handful of utility functions that get the job done. These (and
all other) utility functions are defined in utils.h. They are
GetRegInt(), SetRegInt(), GetRegString(), SetRegString(), GetRegData(),
SetRegData(), and bRegEntryExists().
Before I go over these, I had better make sure you understand
a bit about the registry. The registry is a big hierarchy of
data. The "leaf" nodes, the ones that contain the data,
are called Entries. And the "branch" nodes are called
Keys. Another way to think of the registry is as an artificial
file system, where Keys are directories and Entries are files.
In fact, you have to use a sort of directory syntax to specify
keys, so I recommend you think of the registry in this way.
The "root" of the registry has a handful of keys.
These are HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT,
HKEY_USERS, etc. For storing user data, you'll only care about
two of these: HKEY_CURRENT_USER and HKEY_LOCAL_MACHINE. Both
of these two branches have a subdirectory (oops, I mean SubKey)
named Software. And under Software, you are allowed to make a
new SubKey with your company's name or whatever, and put your
data in there. For instance, Adobe Acrobat's settings are stored
under HKEY_CURRENT_USER\Software\Adobe\Acrobat.
What's the difference between HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER
? The data in HKEY_LOCAL_MACHINE will be visible to all users.
But the data in HKEY_CURRENT_USER is specific to each individual
user... each user has a completely different set of data in their
HKEY_CURRENT_USER. Thus, you'll want to put installation-specific
data into HKEY_LOCAL_MACHINE. For instance, the directory where
the program's data files reside should probably be in HKEY_LOCAL_MACHINE.
But other data, such as the keyboard layout or the user's name,
should be in HKEY_CURRENT_USER, so that each user has their own
copy of the data.
Let me give a concrete example before going further. Say I
want to place into the registry the name of the latest file the
user opened. Let's say the char array LastFileOpened has the
filename in it.
|
|
SetRegString("Software\\MyCompany\\MyProgram", "Last File Opened",
LastFileOpened, HKEY_CURRENT_USER);
|
The first parameter indicates which subkey to put the data
into. The second parameter indicates which Entry to put the data
in. The third parameter is the data itself. And the fourth parameter
indicates which major branch it's under (typically HKEY_CURRENT_USER
or HKEY_LOCAL_MACHINE).
You may be going, "Whoa there, why didn't you put HKEY_CURRENT_USER
into the string of the first parameter? Also, why are there two
backslashes between each subdirectory? I mean subkey?"
Good questions. I think you *can*, in fact, put "HKEY_CURRENT_USER"
into the string of the first parameter. I think the newer versions
of Windows made it so that works. But actually, internally, HKEY_CURRENT_USER
isn't a string name; instead, it's a #define for an ID number.
This is done for optimization's sake.
As for the double backslashes, that's just a C thing: whenever
you want to have a backslash in a literal string, you have to
type it twice.
You read stuff to the registry similarly, but with some additional
parameters:
|
|
char LastFileOpened[ 512 ]; //a buffer for the string
GetRegString("Software\\MyCompany\\MyProgram", "Last File Opened",
"C:\\", LastFileOpened, sizeof(LastFileOpened), HKEY_CURRENT_USER);
|
That new third parameter is the back-up value. If there is
no such registry entry as the one you asked for, your buffer
will instead be given this string. This is often handy, since
the first time your program runs, the registry keys it needs
won't yet exist (unless your install program put them there).
Try using this function to write some data, and then use regedit
to see your data and confirm that it went to the right place.
There are also Get/Set functions for integers, and for generic
data, and these work in pretty much the same way. See the help
file for the exact ordering of the parameters for each of these.
So. That's that in a nutshell. But let me explain a handy-dandy
technique. Instead of sweating over whether each individual registry
entry should be in the user data or the local-machine data, you
might prefer to just look both places.
Here's a scenario. You have an install program that writes
a bunch of configuration strings in the registry. Now, you want
each user to have these configuration strings the first time
they start your program, but when they change them, you want
their changes to be saved for them only.
So here's what you do. You call GetRegString() or GetRegInt()
or whatever and instead of passing it HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE
for the last parameter, you instead pass it KEY_CURRENT_USER_OR_ELSE_LOCAL_MACHINE.
So now the function will try to get the data from HKEY_CURRENT_USER.
But if it isn't there, it will try HKEY_LOCAL_MACHINE instead.
And when you write data, you would write it to HKEY_CURRENT_USER,
so that it doesn't override the "default" settings
that are being stored in HKEY_LOCAL_MACHINE.
So this is how this scenario might play out. Say you want
to get the user's name. The install program prompts them for
their name, and then puts it in HKEY_LOCAL_MACHINE\Software\RabbitoSoft\Rabbit
Emulator 5, under an entry named "User Name". So:
|
|
class TRabbitEmulatorWin : public TWin
{
public:
char UserName[ 512 ];
TRabbitEmulatorWin(TWin *PParent, const TWinAttr &Changes)
: TWin(PParent, Changes)
{
SetMenu(IDM_MYMAINMENU);
}
virtual void OnInit();
virtual void OnDone();
virtual bool OnCmdExecute(unsigned int ID);
};
void TRabbitEmulatorWin::OnInit()
{
GetRegString("Software\\RabbitoSoft\\Rabbit Emulator 5", "User Name",
"Happy User", UserName, sizeof(UserName));
}
void TRabbitEmulatorWin::OnDone()
{
SetRegString("Software\\RabbitoSoft\\Rabbit Emulator 5", "User Name",
UserName);
}
bool TRabbitEmulatorWin::OnCmdExecute(unsigned int ID)
{
if (ID == CMD_CHANGE_USER_NAME)
{
char temp[ 512 ];
strncpyz(temp, UserName, sizeof(temp));
if (InputBox(hWindow(), "What should your new user name be?",
"Change user name", "Name:", temp, sizeof(temp)) == 1)
{
strncpyz(UserName, temp, sizeof(UserName));
}
return true;
}
else
return false;
}
|
I normally put those functions in the class header in this
tutorial, don't I? Oops. Well, this is how you would actually
do it anyway. The functions that aren't in the class declaration
would be in the CPP file.
So this example expects you to have a menu with an option
that has ID CMD_CHANGE_USER_NAME. When this menu item is selected,
it prompts the user to change the "user name." That's
all the program does. Well, of course, it also gets and sets
the user name from the registry using the method described above.
This turns out to be a simple way to do things, even if you don't
use an install program to pre-initialize the registry for you.
You will notice a few things about this example. First, you
may notice that I used the utility function InputBox() to get
a string of input from the user. If the user pressed "OK",
then I put the text that the user typed into the UserName variable.
InputBox() is documented in the online help; it is declared in
utils.h. It's a handy function, especially for prototyping things
and what-not, when you don't want to be bothered to make a dialog
for something simple.
Another thing you might notice is that my GetRegString() call
doesn't include the parameter HKEY_CURRENT_USER_OR_ELSE_LOCAL_MACHINE.
This is because that's the default. If I don't specify anything,
this automatically gets used. Similarly, I don't include the
last parameter to SetRegString(), because the default parameter
is HKEY_CURRENT_USER, which happens to be what I want.
So
how do I get a file-open dialog in my program?
Well, you could write your own dialog, of course, but you've
no doubt noticed that most programs use the same dialog for this.
This is one of a setof "common dialogs" that Windows
provides for us. The file-open dialog is accessed via TFileOpenDlg,
and the file-save dialog is accessed via TFileSaveDlg. These
two dialogs are nearly identical. Hence, TFileSaveDlg derives
from TFileOpenDlg and only makes minor changes.
So how do these dialogs work? Well, simply put, you create
a TFileDlgData structure, fill it out with initial data, and
then create the TFileOpenDlg like you would any other modal dialog.
When the dialog is ended, you check the result-code of Create()
to see if the user pressed OK. If the return-value is 1 (which
is IDOK), then they did press OK, and now the TFileDlgData
structure will contain the name(s) of the files they chose. To
get these names out, you call TFileDlgData::GetFileName(idx),
where idx is the zero-based index of the name. To find out how
many filenames the user selected, use TFileDlgData::GetNumFileNames().
However, normally the user can select only one file, so this
point is moot. If you want to allow the user to select more than
one file, you need to specify the OFN_ALLOWMULTISELECT flag in
the TFileDlgData's DlgFlags field.
Here is an example of a file-open dialog that allows only
one file to be
selected:
|
|
virtual void TMyWin::OnInit()
{
TFileDlgData Data;
Data.FilterList = "Data Files|*.DAT|Any File|*.*";
TFileOpenDlg OpenDlg(this, Data);
if (OpenDlg.Create() == IDOK)
InfoBox("Selected a file! %s", Data.GetFileName(0).c_str());
}
|
Notice that you set FilterList to a string that contains the
various filters you want to provide. The syntax is "name|*.EXT|name|*.EXT|name|*.EXT"
etc, where name is a friendly textual name of the file type,
and EXT is the file type's extension. You should always provide
at least two extensions: the data type you care about, and a
*.* extension. It is a friendly thing to provide the *.* extension,
in case the user has renamed one of their files.
To get a file-save dialog you do exactly the same thing, except
you use a TFileSaveDlg instead of a TFileOpenDlg. I mean that
you pass it the same type of TFileDlgData structure, filled out
the same way.
Allowing the user to select multiple filenames in a file-open
dialog is a very easy alteration:
|
|
virtual void TMyWin::OnInit()
{
TFileDlgData Data;
Data.DlgFlags = OFN_ALLOWMULTISELECT;
Data.FilterList = "Any File|*.*|Data Files|*.DAT";
TFileOpenDlg OpenDlg(this, Data);
if (OpenDlg.Create() == IDOK)
{
for (int loop = 0; loop < Data.GetNumFileNames(); loop++)
InfoBox("Selected %d files! File #%d is %s",
Data.GetNumFileNames(),
loop+1,
Data.GetFileName(loop).c_str());
}
}
|
And that's all there is to it. Another trick: the TFileDlgData
retains other miscellaneous state information like the last filter
the user chose, the directory they were in, and so on. This means
that if you use the very same TFileDlgData structure for multiple
TFileOpenDlgs, the state information will automatically be saved
from dialog to dialog. In other words, if you want to be extra-friendly
to the user, you can make the TFileDlgData a member variable
of your class instead of making it a temporary variable. This
will let the user's choices be persistant for the life of the
window.
For more help see the help for TFileDlgData and TFileOpenDlg.
Now
I want a really FANCY file-open dialog with all kinds of add-on
crap.
This is hard. But okay, there are several ways to proceed.
One way is to provide your own unique dialog-template resource
for the TFileOpenDlg. You must create a child-dialog template
containing only the controls you want to add to the layout. You
will pass the name of this resource to the TFileOpenDlg when
you construct it, and when the dialog is created, your controls
will be smashed into the regular dialog.
It will take some fiddling to get a resource that works right.
Start simple, with just one control, say.
Anyway, once it is working, then when you Create() the dialog,
it will combine your controls into the standard template. Any
controls you put on this template are your responsibility. So
you will need to overload TFileOpenDlg and add code to support
your new controls. You can add initialization and cleanup code
and whatnot in the TFileOpenDlg's OnInit() and OnDone(), and
handle button-presses and whatnot by overloading OnCmdExecute()
-- just remember to call the base-class version of any OnXXX
function you overload, so that the default file-open code will
still be performed.
(This answer is still under development... need an example,
and to explain that your template will (may?) be a child dialog
in the file-open dialog.)
What's
a TFile? How do I use it?
A TFile is the base-class for our file objects. Remember that
EWin is old, circa 1995 old. It predates the non-buggy C++ file
streams. I am considering switching over to the C++ stuff when
I understand it better, but even if I do I will leave the TFile
stuff around for compatibility.
TFile is an abstract base class. There's no point in creating
one, as it doesn't have an Open function, so you could never
open a file. You can use a TCFile or a TWFile to make a simple
file object. "TCFile" uses the C functions like fopen,
fwrite, etc. "TWFile" uses the Windows API functions
like CreateFile, WriteFile, etc.
So:
|
|
TCFile File;
char text[ 512 ];
File.Open("myfile.dat", "rw");
File.GetS(text, 512);
|
This code reads the first line of myfile.dat into the buffer
named 'text'. See the help for the rest of the functions.
What's the use of these classes? Why not just use FILE *s?
Because there are other types of TFiles, like network sockets
and resource streams, and any function that accepts a TFile reference
will be able to use these types of file-like things just as if
they were files.
For instance, if you want to load a bitmap from a file, you
would make a TCFile with the bitmap file's name, and then pass
that TCFile to a TDib's Create():
|
|
TCFile File;
File.Open("Mybitmap.bmp");
TDib Bmp;
Bmp.Create(LoadFromFile, File);
|
And you can use this same Create() function for any other
class derived from TFile, such as TSocket. We use this to stream
bitmaps directly across the network. It's slick.
MORE: Actually, because we very often want to load bitmaps
from a file, there is another version of TDib::Create() that
takes a file NAME instead of a TFile reference. So you could
have consolidated all that code into:
|
|
TDib Bmp;
Bmp.Create(LoadFromFile, "Mybitmap.bmp");
|
For more info, look up TCFile, TWFile, and TMemFile.
[Advanced]
What's the difference between WM_USER and WM_APP?
These two constant denote ranges of values available for user-definable
Windows messages. WM_USER is for window-class-specific messages,
and WM_APP is for application-specific messages. Be careful of
sending WM_USER messages to dialogs, because dialogs already
use the first few WM_USER messages, for BM_SETRESULT and whatnot.
A bad Microsoft decision. But anyway, you might want to start
at WM_USER+100 or so.
For examples of sending and posting user-defined messages,
see EWin_Threads_Tutorial.txt.
This concludes the main tutorial. There's more in the .zip
file... stuff about threads and advanced topics. Pretty soon
I'll add that content to the HTML version. |