Welcome back to yet another article of my hand. If you didn't catch the last one, it is still in the
archive. Of more importance is that I will be speeding up a bit. The first few issues I have dedicated
to getting started. I have also shown you how you should begin a BeOS application. However, every
application has its special needs, and unfortunately I can't quite cover them all.
Luckily there are still quite a few concepts we need to conquer before I can let you loose into the wild.
First of all, there is the concept of messages, which will be discussed in this issue, as well as the
next. Secondly we will need to go over threads. The latter might send some chills through
the spines of some, however, using threads in BeOS is easy and efficient, and as soon as you
have opened the concept of them, you are ready for some genuine BeOS action.
So what are we going to do to Beatrice this time? First of all a note to people who have
downloaded the source and looked through it. There was a CVS class there. You might have
noticed I have not yet used it. The
reason it is there is that I needed some place to play with opening a terminal app that doesn't
have a BApplication object. Normally it is quite easy to spawn other BeOS apps through the
BRoster (if you have some spare time on your hands, look through it). However, it is impossible
to do that to terminal applications. Also I needed to catch the terminal output. The CVS class is
not in the zipfile this time.
What we are going to use this week is the storage kit. We have already gotten acquainted with
the Interface and the Application kits, now we will change to the Storage Kit. This kit deals with all
aspects of files and directories and how they are stored on our disk. We can access files with it,
and we can also 'watch' directories for changes. This means that we will get notifications when, for
example, files change in a directory, or files are added to this directory. You can imagine that this
suits all sorts of purposes in a wide range of applications.
We will also start using BMessages. Messages are an important concept in programming with the BeOS
interface and we can approach many difficult programming problems with them to provide a proper
solution. So let's get started!
Menu items and messages
An important aspect of a GUI is the interaction between two objects. We have, for example, the
window and the button in it. The window contains the functionality of what should happen when
the button is clicked. To let the window know that the button is clicked, the button object should
call a method of the window directly. However, because this means that virtually every GUI element
should be subclassed, a far more powerful means has been designed: messages.
In order to have a messaging concept you need a few things. First of all, you need a class that
is the message itself. It must be possible to have different types of messages. It should also be
possible that messages contain additional information, in the form of variables, to give additional
parameters to the receiver. All of this functionality is encapsulated in the BMessage class.
You would also need a class that would know how to act on the messages it receives. This is done by
BHandler.
Last of all, you would need a class that knows how to receive messages and that takes care of
actually acting based on the type of message. This is done by BLooper. This class creates
a seperate thread that waits until messages are received and then sends them to the BHandler.
The messaging architecture sounds fairly abstract, however, by giving concrete examples I will try
to show how exactly messages work. Actually, we have been working with messages from the
beginning on. Do you remember the BeatriceMainWindow::QuitRequested()
method?
Here it is:
bool BeatriceMainWindow::QuitRequested()
{
be_app->PostMessage(B_QUIT_REQUESTED);
return true;
}
If you look at BApplication, you will see that it inherits both BLooper and BHandler. What we are doing
in this method is actually sending a message, thus we are using the BLooper functionality. You will see that the
BLooper::PostMessage(BMessage *)
method puts the message
passed on into the message queue. The line also introduces you to another concept called the message
type. Because it actually makes sense to have different messages, every message gets an identifier.
This identifier is a unsigned int, in BeOS terms an uint32
. The B_QUIT_REQUESTED
constant is a number that always indicates that the message is a Quit message. What happens is
that BApplication receives the message, then processes it as a call to quit. It then deletes all the windows
and ends the message loop.
I also have extended the menu bar to contain another entry labelled 'Open', which can be used to
select a CVS repository. Let's look at the code of the menubar.
1 //File Menu
2 m_filemenu = new BMenu("File");
3 m_menubar->AddItem( m_filemenu );
4 m_openaction = new BMenuItem("Open", new BMessage( MENU_OPEN ) , 'O' );
5 m_filemenu->AddItem( m_openaction );
6 m_openaction->SetTarget(this);
7 m_quitaction = new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED), 'Q')
8 m_filemenu->AddItem( m_quitaction );
9 m_quitaction->SetTarget(this);
NOTE: The line numbers aren't present in the real code!
In line four we create the new action. The second argument (which you can compare to line seven)
is the message we would like to receive when the menu option is chosen. Note that the MENU_OPEN
constant is defined by me in Constants.h
as const uint32 MENU_OPEN =
'MFop'
. If you define all your constants this way (with four characters closed in by
single quotes), you can be pretty sure that your constants are unique in your application. Also note
line six where we say that the message should be send to 'this'
, so to this window.
So, what happens when the user selects the Open menu entry? Every class that inherits BHandler
has the void BHandler::MessageReceived( BMessage * )
method. This virtual method
can be overridden by us to check if the messages send to the object are perhaps some of our own
defined so that we can act on them. Look at the example of BeatriceMainWindow:
void BeatriceMainWindow::MessageReceived(BMessage *message)
{
switch(message->what)
{
case MENU_OPEN: // The open action was chosen
m_openpanel->Show();
break;
default: // Else see if BWindow can handle it
BWindow::MessageReceived( message );
}
}
You can see that we use a switch
statement to determine what message we have
here. The what
variable of a BMessage is the public uint32 that determines its type.
If the message states a MENU_OPEN message, then do the appropriate thing (what it is I will tell later),
else let the default BWindow::MessageReceived go over it. This last thing is important. There will be
other messages sent to your window that need to be processed in order to make your window
functional, so don't forget to add a default case.
There are more advanced tricks to be played with messages, but we'll do that next time when we will
be implementing some actual CVS processing code!
The Storage Kit: Some concepts
The second topic of this issue is the storage kit. In the BeBook there is a nice introduction on file
systems, so if you aren't at home with file systems, read it. The concepts used in this article
will not go beyond the basic knowledge of files and directories. But the way of thinking in BeOS is a
bit different than you might be used to, so I will explain some pieces to you.
What we need to do is the following. First of all, we need to open a directory that we can process.
Secondly, we need to check if the directory contains
a CVS subdirectory (this CVS directory is present in all repositories). We use BDirectory
and BEntry for this. Finally, we need to build the listview. But before I will show you the
implementation, we first need to get a grip on entries and nodes.
Entries are references to the physical hard disk. Entries are related to the path. So the file
/boot/home/file has an entry, as well as the directory /boot/home/config. The contents of the file
and directory can be accessed via nodes. This may sound unclear, but you use nodes to change
files. You also need nodes to change the contents of directories (for example to create new files).
The 'Open' Dialog
BeOS has a very convenient way of opening files for the users. All 'open' dialogs look and behave the
same. For developers it means that they get a free functional dialog that needs little tweaking. The
class we will be using is the BFilePanel class. Here's the prototype for the constructor:
BFilePanel(file_panel_mode mode = B_OPEN_PANEL,
BMessenger* target = NULL,
entry_ref *panel_directory = NULL,
uint32 node_flavors = 0,
bool allow_multiple_selection = true,
BMessage *message = NULL,
BRefFilter *filter = NULL,
bool modal = false,
bool hide_when_done = true)
It is a pretty tweakable class, and it would take too long to explain all the options. Instead you can look
it up in the BeBook. I have added a BFilePanel object to the BeatriceMainWindow class. Here's the
part of the constructor that initialises it:
//Construct the open dialog
m_openpanel =
new BFilePanel(B_OPEN_PANEL, // It's an open dialog
NULL, // We want be_app to receive messages
NULL, // No specific wishes for where the dialog starts
B_DIRECTORY_NODE, // You only can select directories
false, // No multiple selection
NULL, // We want all dirs
NULL, // No custom messages
true, // Modal panel
true); // Hide when done
We have instructed it to be a B_OPEN_PANEL, the message that contains what directory is selected
is sent to be_app the user will only be able to open directories, with single selection, we will not filter
out any directories, we want the ordinary B_REFS_RECEIVED message to be sent, we want a modal
panel (e.g. the user can't do anything else with the application while the open panel is on screen) and
the panel should hide when the user has selected something and the message is sent.
As you have seen above the panel is shown when the user selects 'Open' from the file menu. The user
selects a directory and a message is sent to be_app. Our BeatriceApplication knows the
B_REFS_RECEIVED message and it will automatically pass it on to the virtual void
RefsReceived(BMessage *message)
method. This method is also called when the user drops
a file on your app. So this method is rather interesting. Look at the implementation I have:
void BeatriceApplication::RefsReceived(BMessage *message)
{
int32 refNum;
entry_ref ref;
status_t err;
refNum = 0;
do {
err = message->FindRef("refs", refNum, &ref);
if (err != B_OK)
return;
m_mainwindow->SetRepository(&ref);
refNum++;
} while (true);
}
The message contains data on what references it carries. Messages may carry more references.
What happens in this loop is that entry_ref
s are taken out of the message and that
these are passed on to a method in our class that is named SetRepository(). What happens is that
every reference is passed on to that same window. There is a huge side note you need to take with
this, because this implementation is quite clumsy. It makes absolutely no sense to have the window
open all references one after another, discarding the previous, in other words, it is futile to pass all
the references on because only the last one will be displayed. But I will introduce a multiple window
architecture soon, so stay put.
Using BDirectories and BEntries
After the user has chosen the directory, the open window will hide again. The next time the user
selects Open, it will be shown again. I bet you can guess the bigger picture here. But what, of course,
is the most interesting here is what BeatriceMainWindow does with this. Prepare yourself, because
here she comes:
void BeatriceMainWindow::SetRepository( entry_ref *ref )
{
1 m_dir.SetTo( ref );
2 if ( m_dir.InitCheck() != B_OK )
3 {
4 m_dir.Unset(); // Clear pointer to repository
5 BAlert *alert = new BAlert( "Error opening repository" ,
6 "The repository you specified is not a valid directory" ,
7 "Cancel" ); // Create Alert that it failed
8 alert->SetShortcut( 0 , B_ESCAPE ); // Map the escape key to the cancel button
9 alert->Go(); // Set off the alert
10 return;
11 }
12
13 // Check if the CVS subdir is present
14 BEntry direntry;
15 if ( m_dir.FindEntry( "CVS" , &direntry , false ) != B_OK )
16 {
17 m_dir.Unset(); // Clear pointer to repository
18 BAlert *alert = new BAlert( "Error opening repository" ,
19 "The repository you specified is not a CVS repository" ,
20 "Cancel" ); // Create Alert that it failed
21 alert->SetShortcut( 0 , B_ESCAPE ); // Map the escape key to the cancel button
22 alert->Go(); // Set off the alert
23 return;
24 }
25 else
26 direntry.Unset(); // Clear open pointer to CVS dir
27
28 // Set the title of the window based on the repository
29 BString windowtitle = "Beatrice: ";
30 char name[B_FILE_NAME_LENGTH];
31 m_dir.GetEntry( &direntry );
32 direntry.GetName( name );
33 windowtitle.Append( name );
34 this->SetTitle( windowtitle.String() );
}
This method accepts an entry_ref
. This is a struct that is the basic type of an entry.
BEntry is a wrapper around an entry_ref that adds some functions to it. The entry_refs we receive
can be virtually anything. Users can only select directories using the open dialog, however, they can
drop anything on our icon using the tracker, so we need to check if the entry we receive is actually
a directory. What happens is the following. In line one we create a BDirectory based on the
entry_ref. BDirectory is a node that handles the contents of directories. It is logical that the object
only constructs if the entry_ref is pointing to a directory. Therefore in line two we call
status_t BDirectory::InitCheck()
to check if the object constructed properly. If it did
not, we notify the user using the BAlert class, the explanation of which is beyond the scope of the article.
Another thing that need to be checked before we can say it is a valid repository, is if there is a CVS
subdirectory present. In line fourteen I create a BEntry. We don't need a BNode, because we only
need to check if it is present, and not what content it has. I use the
BDirectory::FindEntry
to check if the entry exists. If it doesn't, in lines seventeen
through twenty-four we tell the user the repository is invalid. If it is valid, in line twenty-six we clear
the entry so that we can use it again.
In lines twenty-eight through thirty-four we set the title of the window to the leafname (which is the
name behind the slash) of the repository. We need to prefix this with "Beatrice: " and in order to avoid
some dirty char calculations, I have used the BString class to do all this without a big hassle.
Because BDirectory is a node, we need to get its BEntry in order to determine its name. Normally a
BNode doesn't know what entry it has, but BDirectory is an exception for some obvious reasons
(which I wasn't able to find out). I get the entry
with the BDirectory::GetEntry()
method. Then I called
BEntry::GetName()
to fetch the name of the entry. Please note that I used a
character array with the length of B_FILE_NAME_LENGTH, because that's the longest a filename
can get, plus one for the termination character. Finally, I set the window title using
BWindow::SetTitle()
.
This introduction to the storage kit is a meagre one, however, you will have some sort of idea what
you can do with it, and I haven't even discussed the parts to change a file. We will get to that later,
meanwhile try to understand what I have shown you this time.
Concluding
As always, here's a short list of what we have done this time:
- Messages are objects that are transfered between other objects in order to notify eachother
of something
- Messages have a constant that describes them: it's a uint32 directly accessible by
BMessage::what
- Messages can contain data
- BHandler is the base class of all objects that handle messages
- Subclasses of BHandler should reimplement the BHandler::MessageReceived( BMessage
* ) method
- BLooper is the base class of all objects that process messages
- To post messages to a BLooper or a BLooper derative, use the BLooper::PostMessage(
BMessage * ) method
- BApplication is a BHandler and a BLooper at the same time
- The B_QUIT_REQUESTED message is the message with the B_QUIT_REQUESTED
constant, which implies that it asks the window or the application to quit
- BMenuItem constructors take as the second argument the message it will need to
send
- Using BMenuItem::SetTarget( BLooper *) you can set an appropriate target where the
message will be sent to when the item is selected.
- The Storage Kit implements all functionality to work with files and filesystems
- The BFilePanel class can be used to provide an open dialog for the users
- Entries represent the location of a file or directory
- Nodes represent the data in a file or directory
- BDirectory represents the node of a directory
- You can set the window title with BWindow::SetTitle( char * )
That's all for today. As always, you can email me. Source can be found here