Showing posts with label MDI. Show all posts
Showing posts with label MDI. Show all posts

Friday, November 04, 2022

Redesigning the 'blogs' program

Over the past few weeks, I've spent a fair bit of time with my 'blogs database' program and have been thinking about various improvements. Roughly speaking, the program (as like many others) can be split into two: the part that deals with entering/storing the blog entries and the part that deals with creating reports about the entries. Whilst the first part was handled well, the second part was totally lacking. The ideas that I've had, such as the 'this day in the blog history' don't fit into the paradigm established by the program, so I realised that I would have to redesign it and rewrite parts (as well as adding new parts).

Yesterday evening, while walking the dog, I was thinking some more and realised that I would like several different functions to output a list of blogs, each showing different data. This would mean using the multiple document interface (MDI) and displaying the same form several times. How would each instance of the form know what to display? I was thinking originally in terms of a temporary table that would hold the chosen data, but I quickly realised that this would not be sufficient. 

In Priority, there is a mechanism called 'link' that creates a new copy of a table; data is inserted into this copy and then displayed. Eventually the copied table is destroyed. For a few moments I thought about creating new tables then dropping them, but after a while I realised that I could greatly simplify this.  I added to the temporary table a new field, 'instance' that would be part of the primary key; each query would get a unique instance and store its data (entries) along with that instance. The form that displays the data would be sent the instance number and so would retrieve the correct data. When the form closes, it deletes the data from the temporary table and frees the instance number for further use. 

I worked all of this out last night and today I was able to spend a few hours working on the program and changing it around. Some parts - for example, choosing tags for an entry - have remained the same, whereas other parts have been slightly modified. The whole 'instances' idea worked perfectly. Using the same idea for tags is a bit more difficult as one query (unused tags) needs to display only tags whereas another query (entries per tag) needs to display both the tags and their counts. After fiddling around a little, I figured out how to handle this.

Another idea for a report was taking one popular tag (e.g. programming) and seeing how many times different tags have been paired with this tag. Then I could retrieve the entries that have the chosen combination of tags and show them in the the 'show entries' form. Expanding the program is much easier now.

Saturday, December 10, 2011

Preventing "MDI creep": how to decascade a child window

I confessed a terrible feeling of emptiness to my Occupational Psychologist (OP) during our weekly meeting yesterday: until the day before, I been concentrating almost entirely on learning Finance, and now that the exam is over, I don't need to know how to price an option nor what the theory of indifference to dividend policy is. "Don't worry", she said, "I'll find you things to fill you up". And so she did (Happy birthday, by the way!).

One side effect with using MDI applications (such as the management program which I wrote and constantly improve) can be described as follows: when the program's first child window is opened, this window will have its top left hand corner placed in the screen's top left hand corner (coordinates 0, 0). The second child window will be opened automatically at 10, 10 and the third at 20, 20. This was a good idea when screen resolution was not particularly high as it makes good use of the screen's 'real estate'. But times have changed; the OP has one of those extra wide screens and she uses it in a mode which provides a huge amount of real estate. All the child windows congregate on the left hand of the screen and the right hand side is unoccupied.

So the OP drags certain child windows to the right hand side so that she can see several complete and unhidden child windows simultaneously. All is fine until a new child window is displayed; within the program I followed each new window creation with the 'cascade' command - and then the personalised display disappears when all the child windows line up again on the left hand side. This was easy to fix by removing the 'cascade' command after new window creation.

But more insidious is that the MDI screen manager (to which we have no access) will create new child windows as if cascade is still in effect. In other words, the first child window will be created at 0,0, the second at 10,10 and the third at 20,20 - even if the second child window has been moved to a location 200, 600. Worse, this behaviour continues even after the child windows have been closed: new child windows will appear at a location which would be consistent with cascading all the previous child windows.

I researched this behaviour a little yesterday afternoon and discovered that it has been named MDI creep. Whilst naming the behaviour makes it easier to talk about, it doesn't make it any easier to solve.

I posted a question of the Stack Overflow site and master programmer David Heffernan gave an answer which pointed me in the correct direction [I often wonder how David actually gets any work done as he seems to be found almost permanently on SO, answering people's esoteric questions]. It took me about an hour of to-ing and fro-ing with his idea before I had something that worked and about another hour of polishing the entire solution.

The solution boils down to three different parts:
  1. How to enable a child window to signify that it wants to be 'uncascadable'
  2. How to universalise this ability
  3. How to prevent the main MDI window from cascading the uncascadable child windows
The answer to the first part is to use each form's system menu; this is the menu that pops up when one clicks on the form's icon sitting in the top left hand corner of the caption bar.  I used to use the system menu in the early days of Windows programming, but haven't touched it in over a decade. Each form has to add an option to the system menu whose effect will be to toggle the 'cascadability' of the form; so the form has to detect when that option has been chosen and to toggle an internal variable. At the same time, it would be good to give visual feedback on the menu what the state of that variable is at any given time.

Here's the article which I used as the basis for developing the code to add the menu option and detect its being pressed. Finding out how to give the visual feedback was a bit more difficult but eventually I found out how to do this (and more importantly, how to do this correctly!). I'll show the complete code a bit further on.

When I originally wrote this code, it was sitting in the form which needed to be decascaded, but it occurred to me that the technique would be much more valuable if I could use it in almost every form and I wasn't going to copy the code fifty times. The solution is to use form inheritance - to define an ancestor form type which contains the code and then define the actual forms to inherit from this ancestor type. This is one of the huge strengths of Delphi (the entire VCL works on this principal) but is something which I have barely touched.

unit ManageForms;

interface

uses Windows, Forms, Menus, Messages;

type
 TNoCascadeForm = class (TForm)
                   private
                    SysMenu: HMenu;
                   public
                    nocascade: boolean;
                    Procedure GetSysMenu;
                    Procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
                    Procedure SetCheck;
                    Procedure SaveCheck; virtual; abstract;
                   end;

implementation

const
 SC_NoCascade = WM_USER + 1;

procedure TNoCascadeForm.GetSysMenu;
begin
 SysMenu:= GetSystemMenu (Handle, FALSE);
 AppendMenu (SysMenu, MF_SEPARATOR, 0, '');
 AppendMenu (SysMenu, MF_STRING, SC_NoCascade, 'Decascade');
 SetCheck;
end;

procedure TNoCascadeForm.WMSysCommand (var Msg: TWMSysCommand);
begin
 if Msg.CmdType = SC_NoCascade then
  begin
   nocascade:= not nocascade;
   SetCheck;
   SaveCheck;
  end
 else inherited;
end;

procedure TNoCascadeForm.SetCheck;
var
 iChecked: Integer;

begin
 if nocascade
  then iChecked:= MF_CHECKED
  else iChecked:= MF_UNCHECKED;
 CheckMenuItem (SysMenu, SC_NoCascade, mf_bycommand or ichecked);
end;

end.
Procedure GetSysMenu first gets a pointer to the form's system menu, then adds two options: the first is a separator bar and the second is the 'decascade' option. When this option is pressed, a system message of type 'SC_NoCascade' will be sent to the form. Of course, the form has to know how to handle this message.

The final line in this procedure (which will be called once during the inherited form's create method) is to SetCheck - this procedure draws (or erases) the check mark next to 'Decascade' on the system menu, according to the value of the variable nocascade. This is initially set in the inherited form by reading a value stored in the registry; if there is no registry value then the variable is false (no check mark).

When the system message of type 'SC_NoCascade' is sent, it is intercepted by the form's WMSysCommand method. As all system menu messages pass through this method, it is vital to check what the actual message is; if it is not the specific message, then it is passed on to the inherited message handler. Assuming that the message received is SC_NoCascade, then first the method reverses the value of the internal flag, then calls SetCheck to have the menu option updated and finally calls an abstract method called SaveCheck (this probably should have been defined as a stub in the ancestor form).

The SaveCheck procedure is located in the inherited form and stores the value of nocascade in the registry. This is necessary, for if a new inherited form were to be displayed, its nocascade variable would contain the value previously stored in the registry - which would be oblivious to the change which has just occurred. This procedure has to be located in the inherited form as it is dependent on specific registry values which the ancestor form cannot know.

Here is part of the code of a form which inherits from TNoCascadeForm:
unit Manage57;

interface

uses
  Windows, Messages,  ManageForms, ,,,;
type
  TShowCallsTree = class(TNoCascadeForm)
  .
  .
  .
  private
  public
   Procedure SaveCheck; override;
  end;

implementation

{$R *.dfm}
procedure TShowCallsTree.FormCreate(Sender: TObject);
const
 myheight = 440;
 mywidth = 816;

begin
 constraints.MinWidth:= mywidth;
 constraints.MinHeight:= myheight;
 with reg do
  begin
   height:= ReadInteger (progname, 'ShowCallsTreeH', myheight);
   width:= ReadInteger (progname, 'ShowCallsTreeW', mywidth);
   nocascade:= ReadBool (progname, 'ShowCallsTreeCas', false);
  end;
 GetSysMenu;
  .
  .
  .
end;

procedure TShowCallsTree.FormClose(Sender: TObject; var Action: TCloseAction);
begin
 with reg do
  begin
   WriteInteger (progname, 'ShowCallsTreeH', height);
   WriteInteger (progname, 'ShowCallsTreeW', width);
   WriteBool (progname, 'ShowCallsTreeCas', nocascade);
  end;
 action:= caFree
end;

procedure TShowCallsTree.SaveCheck;
begin
 reg.WriteBool (progname, 'ShowCallsTreeCas', nocascade);
end;
After this excursion into the worlds of system menus and inherited forms, we can know address the issue for which we have gathered: how to decascade a child window.

David says in his answer: looking at WM_MDICASCADE it has an option to skip disabled MDI children from cascading. So you could disable certain child windows, send a WM_MDICASCADE message yourself and then re-enable the child windows. Probably easier said that done. The program's main form has a method called cascade, but this is an encapsulation of the WM_MDICascade message and leaves no room for manoeuvre. It has to be replaced by sending the actual WM_MDICascade message, and this message has to have the parameter 2 in order to ensure that disabled windows are ignored.

The main form iterates over the child windows, looking for a form which is decended from TNoCascadeForm; if its nocascade variable is set to true, then the form is disabled. Once this has been done, the form can send the above message to its MDI container window, which handles the cascade. Then the form iterates once more over the child windows, re-enabling those which had previously been disabled.

procedure TMainForm.mnCascadeClick(Sender: TObject);
var
 i: integer;

begin
 for i:= 0 to MDIChildCount - 1 do
  if MDIChildren[i] is TNoCascadeForm
   then if TNoCascadeForm (MDIChildren[i]).nocascade
    then TNoCascadeForm (MDIChildren[i]).enabled:= false;

 sendmessage (mainform.clienthandle, WM_MDICASCADE, 2, 0);

 for i:= 0 to MDIChildCount - 1 do
  if MDIChildren[i] is TNoCascadeForm
   then if TNoCascadeForm (MDIChildren[i]).nocascade
    then TNoCascadeForm (MDIChildren[i]).enabled:= true;
end;
The proof is in the pudding - this really works! Thank you, David, for the help in pointing the way.

Thursday, December 23, 2010

Size matters

Once upon a time (in fact, until about three months ago), the computer monitors on which my programs run were all the same size. This made it very easy for me when designing screens/forms for my programs: if they looked ok on my monitor, then they would be ok on my client's monitors.

With the advent (and purchase) of different sized monitors with different display ratios and resolutions, this state of affairs is now in the past. The client (OP) has a monitor which is capable of displaying many more pixels than my monitor displays, and she wants to make use of that extra space. So I had to start learning about resizeable forms. As it happens, these work in synergy with MDI child forms - but not with dialog boxes - and a few of my recent programs follow the MDI model, so the possibility of conversion to resizeable forms definitely exists.

The starting point of this discussion will be a simple MDI child form with a database grid and a panel hosting a few buttons. Something like this:


My first attempts at creating a resizeable form included the following steps:
  1. Change the form's border style from 'bsSingle' to 'bsSizeable'
  2. Add a status bar at the bottom of the form in order to show the 'stretchable' bitmap hint
Whilst the form could be resized, the individual components of the form stayed where they were. The next stage in the learning process was the anchor: both the grid and the panel have their anchors set to akLeft and akTop, or in English, the left and top coordinates remain at a fixed offset from the form's top and left. As resizing is generally to the bottom and to the right, the result is that the components don't resize. The key to this conundrum was to set the akBottom anchor property to true - the bottom of the component will be at a fixed offset from the form's bottom. If the form's height increases, the height of the components will increase as well.

Whilst all was good at the design stage, running the program produced different results when resizing the form, specifically regarding the width of the form. A MDI child form whose border style was 'bsSizeable' was displaying at a different width than the form's defined width and this was annoying. I programmed a work around by forcing the form to be a specific width in the FormShow and FormResize methods, but it was clear that this wasn't the solution.

Looking around on the internet, I came across the SizeGrip component; I think that this component originated here, although I actually found it on a different blog which quoted the original word for word without attribution (very bad practice). The sizegrip component allows a form to be resizeable without the need of a statusbar; for reasons which I don't understand, the form's border style can remain bsSingle yet still resize, thus solving the display width problem.

Along with anchors, one is supposed to use the constraints property which define the minimum size for the form. Once I had allowed a form to resize, it seemed only polite to store the resized height in the registry so that the form could be displayed at the same height the next time it was invoked. As a result, I started adding the following code to units
procedure TDoTables.FormCreate(Sender: TObject);
const
 mywidth = 416;
 myheight = 496;

begin
 height:= reg.ReadInteger (progname, 'dotables', myheight);
 constraints.MinWidth:= mywidth;
 constraints.MinHeight:= myheight;
end;

procedure TDoTables.FormClose(Sender: TObject;
  var Action: TCloseAction);
begin
 reg.WriteInteger (progname, 'dotables', height);
 action:= caFree;
 TrimMemory
end;
'DoTables' is the name of the form; the registry stores the height of each individual form.The 'myheight' and 'mywidth' constants were defined in the design stage - what looks good on my monitor.

But: there is still room for improvement. If I set the grid and panel's anchor.akBottom propery to true and enlarge the form, the form looks as follows


Whilst the panel has resized, the buttons have remained in the same position. Obviously the top button should remain in the same position, but the second and third buttons' position should change. The bottom button's top property can easily be calculated as it will be the panel's new height less the height of the button less 8 (the offset from the bottom of the panel), but what about the middle button? And what happens if there are four buttons on the panel? Basically, what is needed is the equivalent of the alignment palette's space equally vertically command. As I don't know how to access this at runtime, I have to mimic its action.

After a bit of muddling around, I found the following method which sits in the form's FormResize method. First of all, calculate the increment to the panel's height (the original design time height is stored as a constant), then divide this height by the number of buttons less one. The second button's top has to increase by this calculated increment, and the third button's top has to increase by this calculated increment times two. Translated into code, it looks like this:
procedure TDoTables.FormResize(Sender: TObject);
const
 ph = 441;

var
 incr: word;

begin
 closebtn.top:= panel1.Height - closebtn.Height - 8;
 incr:= panel1.height - ph;
 if rankbtn.Visible then  // five buttons on screen
  begin
   incr:= incr div 4;
   editbtn.Top:= 106 + incr;
   contactsbtn.top:= 204 + incr * 2;
   rankbtn.top:= 302 + incr * 3
  end
 else
  begin
   incr:= incr div 3;
   editbtn.Top:= 138 + incr;
   contactsbtn.top:= 269 + incr * 2;
  end;
end;
The above is probably more complicated than need be, but I thought I'd quote the form's code ad verbatim. The complication is caused by the fact that sometimes the form displays five buttons (rankbtn will be visible) and sometimes only four buttons (no rankbtn). Editbtn is the second button on the panel and contactsbtn the third; Closebtn is always the final button on the panel so its top can be calculated without knowing the increment in panel height. The magic numbers in the code (106, 204, 302, 138, 269) are the buttons' tops at design time.

I'm going to let this code sit for a while (like good wine); maybe there is room for further improvement.

Friday, July 09, 2010

The in-basket 2/A

Whilst reading my previous post, it became clear to me how the duplicate email had been created. There was a letter in the inbox; I double clicked on it in order to open the letter and see its contents. At a later stage, I double clicked on the letter in the inbox again, thus opening the letter a second time. This means that I shall have to add some code to check when double clicking on a letter whether it is currently open; if the letter is open, then the focus should move to this form. As the forms are anonymous (see the excrutiating detail in the previous post), this is going to be more than difficult.

I am reminded of Robert Silverberg's humourous tale of time travel, 'Up the line', when a slightly careless Time Courier manages to duplicate himself.....

The in-basket 2

Last week, after developing what I thought was a very neat solution, I presented my work-in-progress, aka the In-Basket exam, to the occupational psychologist (OP). Whilst she was impressed, it transpires that the direction that I had taken was not the one that she wanted. She insisted on having an interface more reminiscent of an email program, with an inbox and an outbox, where letters move from one to the other. It also became clear that I would need a table of 'dramatis personae', data about the 'people' who sent letters to the examinee to be dealt with.

On Saturday I worked for several hours on the program, getting it to a fairly reasonable state. On Sunday, I worked on the 'reply' section, which was fairly difficult. Unfortunately, demands of the day job and the July heat imposed themselves on me and so I couldn't continue with the work until yesterday evening, when I completed the 'reply' code (it's not exactly how it should be, but it's good enough for now).

This morning I presented the new, improved program once again to the OP and this time got the seal of approval. Even so, there were a myriad of small changes needed, along with a major change: instead of using listviews with which I displayed the messages, I decided to be traditional and used dbgrids. This simplifies certain aspects of the code and makes it more dependable.

I've just finished about four hours of work on the program, in which I seem to have rewritten about 50% of the code. In order to 'celebrate', I entered into the database an exam in which the examinee receives about 15 emails and has to decide in which order to execute the various tasks. The exam program as written isn't totally conducive to this exam; there's no simple way to show the execution order and the emails don't have much content. Our exam is different from others which I have seen in that the examinee has to provide reasons for the actions which he took. I utilised this in order to provide the timetable for my actions.

This specific exam didn't utilise any of the 'reply' code (ie reactions to 'emails' which have already been sent) so I couldn't exercise this part of the code. Otherwise the exam program behaved very well, except for the strange fact that one email appeared twice. I'm not sure how this came about and I'm not sure how I'll debug this.

Whilst developing the program and utilising the 'instant message' feature, I noticed that an instant message was appearing twice. It took me a long time to figure out where this was coming from - as I'm writing what is effectively a real-time program with MDI forms, it's not always clear who is doing what. Eventually I unravelled the mystery: the main program has a timer which fires every minute sending a message to the inbox form, telling it to load any new messages. In MDI programs, the child forms are generally anonymous, so one has to write code like this
for i:= 0 to mdichildcount - 1 do
 mdichild[i].close
When one wants a specific form to execute something - for example, the inbox and outbox forms are unique and can receive messages, whereas the email forms are non-unique and don't have messages - one has to use a typecast. First one checks whether the child form is of the required type and then a typecast is used. I have seen similar code which does not use a typecast, instead using the 'as' keyword; at this stage, the type of the child is known and so it is perfectly safe to use a typecast, which requires less code and is faster than the 'as' approach.

My code:
for i:= 0 to mdichildcount - 1 do
 if mdichild[i] is TSomething
  then TSomething (mdichild[i]).DoSomething;
'As' code;
for i:= 0 to mdichildcount - 1 do
 if mdichild[i] is TSomething
  then with mdichild[i] as TSomething
   DoSomething;
In the specific case of the instant messages, the IM was displayed as a new MDI child form and unfortunately the newest child form becomes mdichild[0], whereas the form which was previously first in the array now becomes second. The timer code was
for i:= 0 to mdichildcount - 1 do
 if mdichild is TInbox
  then TInbox (mdichild).openmessage (minutes);
At the time of execution, mdichild[0] was the inbox and mdichild[1] was the outbox. The first iteration sent a message to the inbox and told it to open any extant messages. When it was the IM's turn, it appeared on the screen. At this stage, mdichild[0] is now the IM, mdichild[1] is the inbox and mdichild[2] is the outbox. On the second iteration of the timer loop, it was looking at mdichild[1] - which is still the inbox! So the IM display code got called again, resulting in two IMs on he screen. The solution was fairly simple:
for i:= mdichildcount - 1 downto 0 do
 if mdichild is TInbox
  then TInbox (mdichild).openmessage (minutes);
Tomorrow I'm going to write the program which displays the user's results. The main report will be in the form of a diary:
00:10 mins: message #1 opened
00:15 mins: message #2 opened
00:20 mins: message #3 opened
00:30 mins: message #3 answered
text of message #3
user's answer
user's reason
00:40 mins: message #1 closed
...etc.

In the test exam, in which all the emails are extant at the beginning of the exam and they have to be replied to in an optimum order, there is no option but to open all the emails at the beginning of the program and read them before one is able to formulate a plan. As it happens, I knew that there were a few emails which could be left unread at first, but I imagine that most examinees won't know this.

Wednesday, June 30, 2010

The In-basket: the initial stages of designing a computerised exam

The occupational psychologist for whom I work (let's call her OP from now on) unveiled her latest project for me a few days ago - computerising the 'In-basket' exam. Although I've never heard of such an exam before, it turns out that it's a well known exercise in management and there are even complete books written about it, such as In-Basket Examination: Test Preparation Study Guide, Questions & Answers. There are many references to this kind of exam on the Internet, but most seem to be connected with branches of the American police. Here's a complete example which has nothing to do with the police.

I feel a little like Jim Graves (or whatever his name was, in "Mission Impossible"): your mission, should you decide to accept it, is to convert this text-only exercise into a computerised simulation.

The original brief which I received very briefly on Friday morning was that the user is presented with a series of emails with which she has to deal. During the course of the exam, more emails appear, and even a secretary enters the room to give details of current events. Apparently the scenario which the OP uses is based on events which happened (or supposedly happened) fifteen to twenty years ago in the chair manufacturing factory in which I work (so I doubt that there is an event in which the secretary rushes in to announce that the factory is on fire).

When considering how to program the simulation, it became clear that the program would need a timer which fulfills the following functions (dig that alliteration!):
  • show the user how much time is left to complete the exam
  • check every minute whether there are new events/emails to be displayed
  • provide a time stamp for actions that the user has taken, so that afterwards her actions can be analysed according to the order in which they were performed
Originally I had assumed that it would be necessary to fake an email application in which some mails are already present, new mails get added during the course of the exam, and others are deleted after they are handled. In order to program this, I used a TListView. It also became clear that the program would have to use the MDI model in order to display all the opened emails, instead of working one email at a time. These are all techniques which I have used in the past few months, so I didn't have any technical problems in programming; the problems which I did have were in designing the interface, or more accurately, determining how the application should be presented to the user, what she would need to do in order to undertake the exam, what actions she would need to take and how these would be represented.

I wrote a pilot version of the exam before I had even seen the Internet material. Upon reading this, I extended the pilot version until I had something interesting - but also somewhat unwieldy.

My method of working was to think hard about how I wanted to implement something (not in the programming sense of implementation, eg how to implement quick sort) and then try and get the program to do what I wanted it to do. The programming per se wasn't hard although at times it was tricky and sometimes I did get bogged down on minor details (for example, in a MDI application, the 'OnKeyPress' event does not work - the solution is to use the TApplicationEvents component).

At this point, I contacted the OP and asked some pertinent questions about the direction of the program - should all the emails be presented at once (as per the example which I gave above) so that they could be prioritised, or should they be presented via the clock, which would make the prioritising much harder. How should the email replies be dealt with? One idea which I saw was to allow the user to 'make' phone calls - how could this be represented?

One idea which I had considered was to show the user's priorities by allowing her to sort the emails in the listview via drag and drop. Whilst waiting for an answer from the OP, I actually implemented this in a small test application. Her reply stated that the priorities would come from the order in which the emails were replied to and thus there was no need for drag and drop.
 
Once I had her reply, I thought a little more about what was needed, especially about how the user would reply to the emails. I had originally designed a simple dialog box (displayed as a non-modal mdi child form) which displayed the original email text in a memo along with two memo boxes connected to a database table in which the user would write his reply and attempt to explain her reasons for replying in this manner. At the bottom of the form were, as usual, two buttons: pressing the OK button would insert the user's answers into the database and would close the form whereas pressing the Cancel button would discard the user's answers and close the form. I had conceptual problems about editing - should the user be allowed to answer the same email twice (or at least edit the original answer)? What would happen to the time stamp in such a situation?

I utilised the time otherwise wasted whilst driving home from work in order to think about the new program structure. I realised that the OK button should initially be disabled; it should only be enabled after the user actually types something in the reply field. Then I considered the fact that if there was no need for drag and drop on the listview, there was no real need for the listview at all. New emails could simply be displayed on the desktop as mdi child forms. To prevent the user from closing the forms without replying (and without the listview, there would be no way of redisplaying the forms), I realised that the Cancel button should be removed!

I dropped the idea of 'recording phone messages' as these seemed to be identical to the 'instant messages', the equivalent of the secretary entering the office and announcing that the workers had gone home on strike. The 'instant messages' were virtually the same as the 'email messages' - the only difference (at the moment) being that the emails were 'sent' by somebody whereas the instant ones weren't. This meant that I could use one database table to store all the messages and display them in the same form (hiding the 'from' field if it were empty).

Upon getting home, I set about writing a new version of the demo exam. The database structure had simplified, so I deleted the original database file and defined a new one. This new version of the exam is - from a programming point of view - much simpler. Apart from a form which is displayed modally at the beginning of the exam in which the user enters her personal data, the program consists of one MDI main form with a timer; every minute, the program executes a query which determines whether there are any messages to be displayed. If so, a call is made to display an MDI child form as described earlier. There is only one button on the form - OK - which is initially disabled. If the user wishes not to act upon the form, it simply stays open on the desktop to be handled at a later stage. If the user types something into the form, pressing the OK button will save the data (along with the time stamp) and remove the form from the desktop - forever.

Now all I need to do is input the scenario  - the messages and when they appear - and the exam, at least in its initial stage, is complete. I am sure that there will be many changes, but most of them will be minor now that the architecture is in place. Of course, I could have misunderstood the OP's intentions.....

Saturday, January 02, 2010

Neat tricks in the 'management' program/2 - MDI

I wrote a week ago about the use of non-modal dialogs, and how I was trapping the OnDeactivate event in order to minimise them. Whilst showing the program yesterday to the client, I discovered an unwanted side effect of this: it was impossible to show two opened child dialogs simultaneously. As one would open, the other would minimise; if I clicked on the second, the first would minimise. Ooops.

In a strange case of serendipity, the evening before found me playing around with MDI forms. I have never used this multiple document interface technique before, but it seems to match perfectly with the non-modal dialogs. Once I had realised my mistake yesterday, I realised that the entire program had to be revised in order to use the MDI model. The original main form did not use a menu, which precluded MDI, so I created a new main form, defined a menu and based all the MDI child dialogs on that menu. To make things easier for the client, the program automatically displays what was the main form upon starting. Of course, I removed from all the child forms the 'minimise on deactivation' event.

The code to activate these forms is becoming simpler and simpler. Contrast the code for a modal dialog box
with TModalDialog.create (nil) do
 try
  showmodal
 finally
  free
 end;

with the code for a non-modal dialog box
with TNonModalDialog.create (nil) do show;

and finally the non-modal MDI child dialog box
TMDIChild.create (nil);

After creating the MDI child, I call the main program's cascade method in order to ensure that the dialogs don't lie on top of each other. Each child dialog frees itself in its OnClose event.

Most articles about MDI assume that the MDI child dialogs will be identical - the easiest example of this for a Delphi programmer is the IDE itself; the windows displaying the code of the different modules are identical MDI child dialogs. But it doesn't have to be like that, and my program shows how to use non-identical child dialogs. Had those articles been clearer on the subject years ago, then I probably would have been a veteran MDI user and wouldn't have had to write this.