
I will not write beautiful prefaces, because the article is not entertaining, but rather technical. In it, I want to turn to the user interface programming techniques of classic Delphi desktop applications in MVC style. This is an introductory article from the subsequent series.
Those few who still use this development environment, I ask under kat.
By classic applications, I mean desktop GUI applications for Windows based on VCL. About the FireMonkey framework, which appeared in new versions of Delphi, let someone else write an article.
User interfaces are very diverse. And if on the web they are a complete menagerie, in desktop applications everything is more conservative. Of course, the developers of some applications (see Skype, Mikogo, Office 2010) continue to come up with all sorts of visual tricks that are designed to further improve usability, for most of us (the people of the old school), we are still more accustomed to the standard Windows and VCL controls that were invented, probably back in the days of Windows 3.1:
- button (TButton)
- tick (TCheckBox)
- switch (radio button, TRadioButton)
- single line input field (TEdit)
- multi-line input field (TMemo or TRichEdit)
- all kinds of combined controls (TSpinEdit, TDateTimeEdit)
- multipage controls (TPageControl, TTabControl)
- grids
- panels, group boxes, bevels, shapes, imageboxes, etc.
The main task when creating a user interface is to come up with a representation of the internal data of the program using the above elements, so that the user is comfortable with this data. Well, or on the other hand, come up with such a set of elements with which the user could tell the program what the program should hear from it.
Designing the interface from the point of view of the layout of the elements is in itself a rather complicated and important task. The interface is the face and the main way for the user to interact with your program (not counting its removal by Ctrl + Alt + Del :)). That is why you should always do interface design iteratively, receiving feedback from users each time, is it convenient for them to work with your program, do you have to click the mouse 200 times, go to the nested windows (which at the last moment are also modal), 10 each just enter the same data due to the fact that they are not saved when you re-enter the window, etc. etc. (I am sure, the sophisticated reader can give a lot more examples of how the user interface should not be :)).
')
They say that smart guys overseas, who approach interface design seriously and thoroughly, also use such technologies as tracking mouse movements when working with the program (so that they don’t take it back), counting clicks and even tracking the direction of the user's gaze (if eyes start to run in random directions, it should already be alarming).
But this article is not about these miracles. Let me suggest that you have already figured out what this window will look like. You just need to somehow link this view with the internal data of the program. And it is about the ways of this connection, I would like to talk.
Let's start with a primitive. Check mark.
Suppose you have a checkbox (TCheckBox) in some window that reflects the choice of one of two options. To talk not about spherical horses in a vacuum, let's give it some meaning. Let it be a window to import some data from files of a specific directory into the database. A check mark will indicate whether to delete imported files after the operation is completed. Then let's call our tick
cbNeedDeleteFiles: TCheckBox;
By the way, prefixing control names based on the type of control is very convenient. For example, cb for TCheckBox, rb for TRadioButton, bt for TButton. If you install the
cnPack extension pack in Delphi, then when placing the next control on the form, a window will pop up with a proposal to immediately rename this control in accordance with your rules. This avoids the dominance of Button87, CheckBox32, etc. appearing on the form.
As a rule, after placing the control in the right place on the form, the programmer sighs with relief and calms down. Now he can, from any place of the program, contact cbNeedDeleteFiles.Checked to find out if a check mark is set or not. Probably, the programmer will not have access to the Checked property in a single place: when creating a window, he may want to set the default state of this property or its stored value, then in the main place (where the import is performed) you need to check this attribute again, and finally, where to save the value of this attribute when the window is closed in order to restore the state of the checkmark the next time the window is opened. You may also need to programmatically change the value of this attribute based on some other conditions. For example, a user may ask that this check box is always set automatically when selecting a directory with input files, if this directory contains only files of one strictly specific type, or if the names of all the directory files meet a certain mask. And so, in a heap of program locations we have something like this:
if cbNeedDeleteFiles.Checked then ... if Something then cbNeedDeleteFiles.Checked := True; if SomethingElse then cbNeedDeleteFiles.Checked := False;
At first glance, nothing bad is not here. But as the long-term practice shows, this is AWESOME. This is called a tight string of code on the user interface. Suppose you subsequently have to replace TCheckBox with two radio buttons: “Delete imported files” and “Do not delete imported files”. This does not make much sense, but you can do it for better visualization or as part of refactoring before adding the third state of this setting, such as “Delete files only if there are no import errors”. And at this moment you will have to insert some code for working with RadioButtons in a heap of places where you used to access cbNeedDeleteFiles.Checked.
How to avoid it?
In the network for a long time and a lot of trumpeting about
MVC ,
MVP ,
MVVM . As if these are such miraculous techniques, following which you can program the user interface "correctly" and not have the crap described above. In fact, these are only approaches that really help, but which can be implemented absolutely differently in different programming languages ​​and even in one language. Those. these are rather tips on which side is better to approach user interface programming.
If you look at the abbreviations again, you can see that in all three there are letters M (Model, model) and V (View, representation). Speaking in a very simple language, the model is the internal data of the program, and the presentation is external (user interface). Returning to the tick, it is obvious that the internal representation of this tick is a boolean value. The decision on which attribute of the class should store this value is made in each case individually. For example, it may be the TConfig class, which provides access to the program settings. However, in simple cases, it is enough to create the corresponding attribute just for the form class:
TfmImport = class(TForm) ... private ... FNeedDeleteFiles: Boolean; public ... property NeedDeleteFiles: Boolean read FNeedDeleteFiles write SetNeedDeleteFiles; end;
Next, you need to associate the state of the NeedDeleteFiles property with the state of the visual component (TCheckBox) cbNeedDeleteFiles. This is conveniently done through the set property method:
procedure TfmImport.SetNeedDeleteFiles(const Value: Boolean); begin if FNeedDeleteFiles <> Value then begin FNeedDeleteFiles := Value; cbNeedDeleteFiles.Checked := FNeedDeleteFiles; end; end;
Why do I need the condition FNeedDeleteFiles <> Value, I will explain later. The main thing is that now when we assign the value to the NeedDeleteFiles property, we will automatically have a check mark (this is almost the MVC model - we change the value of the model element, and the view changes automatically). But this connection is only in one direction - from internal data to the interface. It is also necessary to achieve feedback - from the presentation (ie, from the tick) to the model. To do this, in the OnClick handler of our checkbox, we will write the following code:
procedure TfmImport.cbNeedDeleteFilesClick(Sender: TObject); begin NeedDeleteFiles := cbNeedDeleteFiles.Checked; end;
Those. an action on a view (in this case, clicking on the check mark) will bring the model into line with the current state of the view. However, the model never trusts the view and therefore causes the view state to be re-brought to the model state (forcing cbNeedDeleteFiles.Checked: = FNeedDeleteFiles to be forced. There is nothing wrong with this. And we are even insured by checking if FNeedDeleteFiles <> Value from a situation that visual control will call the OnClick handler again. In fact, it will not do this, because there is a similar check:
procedure TCustomCheckBox.SetChecked(Value: Boolean); begin if Value then State := cbChecked else State := cbUnchecked; end; procedure TCustomCheckBox.SetState(Value: TCheckBoxState); begin if FState <> Value then begin FState := Value; ... end; end;
Now we have the state of the TfmImport.NeedDeleteFiles property in sync with the state of the cbNeedDeleteFiles.Checked daw in both directions. In all places of the program where we used to access cbNeedDeleteFiles.Checked, now you should access the NeedDeleteFiles property. This allows us to completely forget that the representation of the NeedDeleteFiles element is a CheckBox. You have no idea how wonderful it is. Subsequently, we can replace the CheckBox with two radio buttons or whatever, and we need to rewrite only the SetNeedDeleteFiles set method (Model -> View direction) and the handler that is triggered when the view state changes, i.e. visual components (direction View -> Model).
I missed such an important moment as the initial synchronization of the value of the NeedDeleteFiles property with the state of the visual component. Of course, if when you open the window, your daw will either be always always set or always removed, you can simply set the correct state in DesignTime, and assign the appropriate value to the FNeedDeleteFiles field in the OnCreate form class. However, this is not very reliable (this should be monitored, it is easy to make a discrepancy), so in OnCreate it is better to place the following code in the form class:
procedure TfmImport.FormCreate(Sender: TObject); begin FNeedDeleteFiles := False;
In the next part of the article I will try to talk about more complex cases: working in MVC-style with lists of elements (TListBox, TCheckListBox, TComboBox) and about pitfalls when memorizing the state of visual elements of a window when it is closed.
UPD. Added the
second part of the articleUPD. Added the
third part of the article