This is a collection of hints, tips and tricks for Delphi programming. I'll add more snippets as they become available.
Error messages
Control '' has no parent window
When you drop a windowed component on a form, it happens some time that the component does not instal and generates the error "Control ' ' has no parent window". This simply means that the constructor of the component contains statements that need a valid window handle. Because he window handle isn't created in the object constructor, and it is created "much" later, in CreateWnd(), move the offending statement(s) into an overridden CreateWnd() method and it will install.
Algorithms
Calculation of the date of occurence of Easter Sunday each year
The date of Easter Day was defined by the Council of Nicaea in AD325 as the Sunday after the first full moon which falls on or after the Spring Equinox. The Equinox is assumed to always fall on 21st March, so the calculation reduces to determining the date of the full moon and the date of the following Sunday. The algorithm used here was introduced around the year 532 by Dionysius Exiguus. Under the Julian Calendar (for years before 1753) a simple 19-year cycle is used to track the phases of the Moon. Under the Gregorian Calendar (for years after 1753 - devised by Clavius and Lilius, and introduced by Pope Gregory XIII in October 1582, and into Britain and its then colonies in September 1752) two correction factors are added to make the cycle more accurate.
The Delphi program that follows calculates the date of occurence of Easter Sunday given the year.
function Easter(Y : Integer) : TDateTime;
var
c,n,k,i,j,l,m,d : Integer;
begin
c:= Y div 100;
n:= Y mod 19;
k:= (c-17) div 25;
i:= (c - c div 4 - (c-k) div 3 + 19*n + 15) mod 30;
i:= i - (i div 28) * (1- (i div 28) * (29 div (i+1)) * ((21-n) div11));
j:= (Y + Y div 4 + i + 2 - c + c div 4) mod 7;
l:= i - j;
M:= 3 + (l+40) div 44;
D:= l + 28 - 31 * (M div 4);
Result:= EncodeDate(Y,M,D);
end;
I don't recall where I found the formula but I know that it calculates the occurrence of the first Sunday following the spring full moon. It is valid only for the Gregorian calendar, i,e., for years after 1753.
Using a different algorithm programmed in PHP, you can find hereunder a way to find the date of Easter given the year:
How to programmatically find a node in a ShellTreeView component
The ShellTreeView component is a window, which displays a hierarchical tree view of the system's shell folders and closely mimics the left-hand pane of the Windows Explorer. Every entry shows the icon and the name of the folder. By double clicking an entry, the user can show or hide a list of the subfolders of the selected folder. Alternatively, the user can click the [+] or [-] buttons in front of an entry.
Technically speaking, the ShellTreeView component is a descendant of TCustomTreeView that implements the IShellFolder interface to communicates with the Windows shell. Each node is comprised of a Data record whose PathName member provides the path leading to the node. It is therefore extremely easy to obtain the path of a node using
Path:= TShellFolder(Node.Data).PathName
but the reverse is not as easy. The process takes the path of a folder as its argument and it returns a reference to the node of the tree that corresponds to this pat but all the work is performed by the internal function ScanNode(). The algorith involves a progressive segmentation of the elements of the path. For instance, the path
C:\Program Files\MyFolder\MySubFolder
contains 4 segments "C:\", "Program Files", "MyFolder" and "MySubFolder" separated by "\". The algorithms is performed in the procedure ScanNode() that is called recursively and stands as follows:
- The procedure takes three arguments:
-
- Path, a string that contains a path starting initially at the root of the shell but each time the procedure is called, the leading segment is removed and put in the second argument Segment;
- Segment, a string that is an empty string initially but is appended with the trailing segments as the procedure is calld; and
- ANode, a reference to a node (initially the root node: Item[0]) ;
- The position of the first separator "\" is searched.
- Given this position, Path is given the value of the leading segment whereas Segment contains the rest of the segments;
- A correspondence to Path is searched in the paths associated with the child nodes of ANode: if it is found, the procedure is repeated with new values of Path and Segments and the reference of the found note as its third argument. If not found, the procedure exits;
- This process is repeated until the segmentation of the path is exhausted and Path is empty. In this case, GetNodeFromPath() returns the reference of the found node if such node was found, nil otherwise.
function TGtroCustomCheckShellTreeView.GetNodeFromPath(const Path: string): TTreeNode;
var
FoundNode: TTreeNode;
function AppendStop(ANode: TTreeNode): string;
begin
Result:= TShellFolder(ANode.Data).PathName;
if '\' <> Copy(Result, Length(Result), 1) then
Result:= Result + '\';
end
procedure ScanNodes(Path: string; Segment: string; ANode: TTreeNode);
var
P: Integer;
Stop: string;
begin
P:= Pos('\', Path);
if P > 0 then
begin
// Split the path into segments (segments are separated by "\")
Segment:= Segment + Copy(Path, 0, P); // First segment of the path
Path:= Copy(Path, P+1, Length(Path)); // Remaining segments of the path
ANode.Expand(False); // Expand the node
ANode:= ANode.getFirstChild; // Move to the first child node
Stop:= AppendStop(ANode); // Append "\" if not already there
while Segment <> Stop do // Iterate on each child nodes
begin
ANode:= ANode.getNextSibling;
if ANode <> nil then
Stop:= AppendStop(ANode)
else // Iterated beyond last child node
Break; // No correspondence => out!
end;
if Path <> '' then // Path is not exhausted
begin
if ANode <> nil then
begin
ANode.Expand(False);
ScanNodes(Path, Segment, ANode); // called recursively
end;
end
else // Path is exhausted
FoundNode:= ANode; // Node found
end;
end;
begin
ScanNodes(Path, '', Items[0]);
Result:= FoundNode; //Reference to the node if found; nil otherwise
end;
Components
VCL Component's Icons
I wandered how to use the icons associated with the VCL components and I searched for an answer on the Web. I found out (Jeff Overcash) that they are stored in the design time packages of the VCL. One can use the resource explorer located in the demos directory to look at and save off resources from dlls (which is all a package really is). The design time packages are in the bin directory and mostly start with dclxxx.bpl.
Miscellaneous
Formatting the display of a TTimeField
I wanted to format time input in a field of a TTable as 'hh:nn'. First, I put the DisplayFormat property of the field to 'hh:nn' at design time and it looked fine. However, during execution, it behaved correctly (e.g. 18:45) until the component displaying the field got the focus (it displayed 18:45:34 instead of the desired 18:45). I found out that this can be corrected using the OnGetText and the OnSetText of the underlying field as follows:
procedure TDM.TableItemsStartItemGetText(Sender: TField;
var
Text: String;
DisplayText: Boolean);
begin
Text:= FormatDateTime('hh:nn',Sender.AsDateTime);
end;
and
procedure TDM.TableItemsStartItemSetText(Sender: TField;
const Text: String);
begin
Sender.AsDateTime := Date + StrToTime(Text);
end
Get rid of flicker with LockWindowUpdate()
As I had implemented a text search over several records of a database, I wanted to find out how I could remove the flicker that occurs when the fields of the database are displayed in succession in a TDBRichEdit component. It was a real disturbance and my first move consisted in hiding the display for the duration of the each search but the approach did not appear to be very professionnal. My second move was to replace the display by a snapshot of itself (using the BitBlt() function of the Windows API) for the duration of the search but during the search I made on the Web, I found the LockWindowUpdate() function of the Windows API and my problem was solved.
Simple, simply use it as in the following code snippet:
procedure TAny.DoSomething(Argument: string);
begin
LockWindowUpdate(Window.Handle); // locks the window and prevents flicker
// the argument specifies the window in which drawing will be disabled.
try ... // do something
finally
// enables drawing in the specified window.
LockWindowUpdate(0);// unlocks the window
end; // try...finally
end;
What does this function do? When a window is locked, all attempt to draw into it or its children fail. Instead of drawing, the window manager remembers which parts of the window the application tried to draw into, and when the window is unlocked, those areas are invalidated so that the application gets another WM_PAINT message, thereby bringing the screen contents back in sync with what the application believed to be on the screen.
There is only one problem with this function. The documentation explicitly calls out that only one window (per desktop, of course) can be locked at a time. since the call to LockWindowUpdate(0) at the end of the procedure would not know which window to unlock if there were many.
Database Programming with the BDE
BLOB has been modified
"Blob has been modified" ($3302) is an error that occurs when the BLOB portion of the record contained in the .DB file has become inconstent with the BLOB portion in the .MB file. This could occur when the write to the .DB file was successful but the .MB file did not get updated, or visa-versa.
There are a few mechanisms to fix a table where these errors has occurred:
- First try re-starting the application. It is possible that the BDE has become unstable and is reporting incorect errors. Also try opening the table with a different application.
- Use Paradox 7 or 8 to run the Table Repair utility. Please see original documentation for more information.
- Run TUtility and rebuild the table. TUtility is an unsupported utility available for download from the Inprise's web site in the Utilities, programs and updates section.
- Delete all indexes and recreate them (Index out Date ($2F02) error only). To do this you'll need to know the structure of all your indexes (including primary) before recreating them, which means you need to know the structure of all indexes before the error occurs.
Avoiding data losses in Paradox tables
In a Paradox database, several records may be created or updated. If the BDE is shut down in an irregular manner, all of the new data is lost. but there is a way to prevent it: write the data to disk immediately after a record is updated. This can be done as follows: add BDE to the uses clause and in the AfterPost event handler, put
DBiSaveChanges(yourTableName.Handle);
This will save all data in buffers directly to the database thus preventing a loss of data should anything go wrong in the current database session.
This may be a way to avoid the "BLOB has been modified" error discussed in the previous section.
Check Table existence
In order to find if a table exists without raising any exception, iterate through the form's Components array property and check for the TTable.
for I := 0 to Pred(ComponentCount) do
if Components[I] is TTable then
if (Components[I] as TTable).Name = 'MyTTable' then ..
or use the FileExists function described in the on-line help.
Copy of the structure of a table
In order to copy the structure of a table to an empty table, use the following procedure:
procedure CopyStructure(Source, Target: string);
begin
with TTable.Create(Application) do
try
TableName := Source;
Open;
FieldDefs.Update;
IndexDefs.Update;
Close;
TableName := Target;
CreateTable;
finally
Free;
end;
end;
Copying a table
When trying to move the data from a table to another table with the same structure, one uses the TBatchMove component. It will even create the table for you if you set the Mode property to batCopy. One thing about using BatchMove to Create or Copy an existing Table, is that none of the Indexes or Keys are copied.
Where Table1 exists and Table2 does not exist, but has a valid DataBaseName (Alias) and a new (typed in) TableName, use the following procedure:
procedure TForm1.CreateTblButton1Click(Sender: TObject);
begin
// This creates Table2 from Table1 with primary and secondary indexes
Table2.FieldDefs := Table1.FieldDefs;
Table1.IndexDefs.Update;
Table2.IndexDefs.Assign(Table1.IndexDefs);
Table2.CreateTable;
Table2.Open;
// This copies all the records from Table1 to Table2
BatchMove.Source:=Table1; // can be set in object inspector
BatchMove.Destination:=Table2; // can be set in object inspector
BatchMove.Mode := batAppend; // can be set in object inspector
BatchMove1.Execute;
end;
Another way to copy a table with all its content, structure, indexes and keys of non-SQL databases is to use dbiCopyTable(). See what Borland proposes.
Compacting a table
The dataset components (TTable, TQuery, etc.) in Delphi are designed to be as generic as possible, for use with the many different table types that can be accessed. This means that some of the operations specific to only one type of table (such as a pack for a dBASE table) would be excess baggage for all other table types. Because of this, these unique operations were not built into the stock dataset components. If you need such operations in a component, you can easily create a custom component that implements the needed functionality or use the BDE functions directly. If it is a dBASE table, you have to use the BDE API function DbiPackTable(). If it is a Paradox table, use the BDE API function DbiDoRestructure() function. The bPack field of the pTblDesc parameter must be set to True to trigger the pack. If the table is of any other type, see the documentation from the vendor for the specific database system.
Find hereunder a procedure that I have developed that packs a Paradox table
procedure TDM.PackTable(Table: TTable);
var
cProps: CURProps;
hDB: hDBIDb;
TableDesc: CRTblDesc;
begin
if Table.Active then Table.Close;
Table.Exclusive:= true;
try
Table.Open; // Open exclusive
Check(DbiGetCursorProps(Table.Handle, cProps)); // Get table properties
if (cProps.szTableType = szParadox) then // Table is Paradox table?
begin
FillChar(TableDesc, SizeOf(TableDesc), 0); // blank out the structure
Check(DbiGetObjFromObj(hDBIObj(Table.Handle),objDATABASE, hDBIObj(hDb))); // Get the Database handle
StrPCopy(TableDesc.szTblName, Table.TableName); //put Table name in structure
StrPCopy(TableDesc.szTblType, cProps.szTableType); //put table type in structure
TableDesc.bPack:= true; // set pack option to true in structure
Table.Close; // close the table so the restructure can complete
Check(DbiDoRestructure(hDb, 1, @TableDesc, nil, nil, nil, false));
end //if
else ShowMessage(strParadox);
finally
Table.Exclusive:= false;
Table.Open;
end;
end;
First, the table must be closed and re-opened exclusive. You get the handle of the table end verify that you are dealing with a Paradox table. Then, the packing process is prepared and executed. After that, the table is closed and re-opened non-exclusive.
MySQL databases
On the Web today, content is king. After you've mastered HTML and learned a few neat tricks in JavaScript and Dynamic HTML, you can probably build a pretty impressive-looking Web site design. But then comes the time to fill that fancy page layout with some real information. Any site that successfully attracts repeat visitors has to have fresh and constantly updated content. In the world of traditional site building, that means HTML files and lots of them
The MySQL database is a free database engine under the GNU General Public License model (although you can also purchase a commercial license) and its amazing how well-known the name of MySQL has become in just a few years. It is available on both Windows and Linux, and is available as the first choice on a large number of web servers. Apart from that, MySQL offers a lot as a relational DBMS, including a very good reputation when it comes to the speed of reading records (other operations are fast as well, but MySQL seems to shine especially when reading data), and support for SQL, client/server development, and even transactions.
Table types
MySQL has six distinct table types (MyISAM, InnoDB, MERGE, ISAM, HEAP and Berkely DB) but two of them attract special interest:.
- MyISAM is the default storage engine for MySQL. If you create a table in MySQL, without paying any attention to select a specific engine, then that table will be created in MyISAM (by default). This storage engine is the best for high-speed storage and retrieval. It also supports fulltext searching capabilities. MyISAM is supported in all MySQL configurations. This engine is used for data warehousing, read-only archives optimized for bulk operations, etc. as it is the best solution when you want flexible index formats, big scans and has a small disk footprint for data and index. This is not suitable for a typical OLTP (Online Transaction Processing) application because of table-level locking, bad crash durability and lack of ACID (Atomicity, Consistency, Isolation, and Durability) compliance.
- InnoDB - is a storage engine for MySQL, included as standard in all current binaries distributed by MySQL AB. Its main enhancement over other storage engines available for use with MySQL is ACID-compliant transaction support, similar to PostgreSQ One of the main advantage of this engine is that you can rollback to a previous version of the database using its COMMIT/ROLLBACK support. InnoDB engine is more reliable than MyISAM.
As my intent was the development of a rather simple database, I have chosen the MyISAM type of table for the purpose of my project despite is lack of support for referential integrity.
Accessing MySQL databases with Delphi
The Borland Database Engine (BDE) has long been the number one choice for quick-and-dirty data access, based on the dBASE and Paradox table formats. But now the BDE is officially frozen and SQL Links is even deprecated. In other words, there will be no further development of and enhancements added to the BDE, so we should be seriously looking at alternatives for data access in Delphi.
There are various way of accessing MySQL from Delphi.
- mysql.pas is a Pascal translation of mysql.h and two other C header files needed for writing clients for the MySQL database server. The unit has been tested and compiles on Borland Delphi 4, 5, and 6. The latest release of the source code is posted at mysql.pas-3.23.49.zip (9.6 KB). To use mysql.pas, you'll need a copy of libmySQL.dll, a library that can be found in the "lib" directory of your MySQL installation (Windows version). If you don't have the library, please feel free to download a copy from libmysql.dll-3.23.49.zip (129.5 KB).
- ODBC/BDE - On the MySQL web site you can find the MySQL ODBC driver for Windows. Download it if you don't have it. After installing this driver you can use the BDE Data Access components or the ADO components to access tables or to perform queries on a MySQL database.In order to use ODBC, first you should create a DSN using the ODBC Data Sources applet in the Administration Tools applet of the Control Panel. For example, I created a DSN named MySQL_Test and configured it to log in as 'root' in the MySQL server located at 'localhost' (you can connect to a remote server using its IP address or its domain name) and open the 'mysql' database. In a Delphi program you would have to use a TDatabase component if you don't want the BDE to show the standard login prompt. You have to set minimally the following properties:
-
- AliasName = 'MySQL_Test'
- DatabaseName = 'MySQL_Test'
- LoginPrompt = False
Catching exceptions in threads
It does not need much experience in Delphi programming to recognize what happens when a procedure with a long loop in it is executed: the application stops receiving messages and appears to hang. The most simple way of dealing with this situation is to make a call to Application.ProcessMessages() within the body of the loop so that the application can still receive messages from external sources. In some cases, this may be practically useless mainly if some steps of the loop take several seconds to execute. In such cases, using threads is the answer. It frees the user interface because the process is running completely separate from the main thread of the program where the interface resides. So regardless of what you execute within a loop that is running in a separate thread, the user interface will never get locked up.
Although the Win32 API provides comprehensive multithreading support, when it comes to the creation and destruction of threads, the VCL has a useful class, TThread, which abstracts away many of the technicalities of creating a thread, provides some useful simplifications, and tries to prevent the programmer from falling into some of the more unpleasant traps that this new discipline provides. The Delphi help files provide reasonable guidance when creating a thread class, so I won't mention much about the sequence of menu actions required to create a thread apart from suggesting that the reader select File | New... and then choose Thread Object.
To create a multithreaded application, the easiest way is to use the TThread Class. This class permits the creation of an additional thread (alongside the main thread) in a simple way. Normally you only have to override 2 methods: the Create constructor, and the Execute method.
There is indeed a problem: If exceptions are raised in a thread that is not the primary thread, and that exception is not handled in code, no dialog box appears to tell the user what happened. The program happily continues while the thread that raised the exception dies. This is not always what the programmer wants. The trick to avoid this is to get the exception shown from the context of the primary thread and that's exactly what the synchronize keyword is meant to do.
I found a solution to this problem in an article on www.wehlou.com. Instead of deriving a thread classes directly from the TThread class, it is proposed to derive it from TThreadGT class as follows:.
type
TThreadGT = class(TThread)
private
fException : Exception;
procedure PrimaryHandleException;
protected
procedure HandleException;
end;
procedure TThreadGT.PrimaryHandleException;
begin
if GetCapture() <> 0 then
SendMessage(GetCapture(), WM_CANCELMODE, 0, 0);
if fException is Exception then
begin
if Assigned(ApplicationShowException) then
ApplicationShowException(fException);
end
else
SysUtils.ShowException(fException, nil);
end;
procedure TThreadGT.HandleException;
begin
fException := Exception(ExceptObject);
try
if not (fException is EAbort) then
Synchronize(PrimaryHandleException);
finally
fException := nil;
end;
end;
Then in your very own Execute() method, make sure TThreadGT's HandleException() method is called:
procedure TMyThread.Execute;
begin
inherited;
try
while not Terminated do
begin
...
end;
except
HandleException();
end;
end;