Thursday, February 14, 2013
Happy 18th Birthday, Delphi!
Monday, February 4, 2013
Delphi Editor Key Combination Table Updated
In May of 2009 I blogged about a table of Delphi editor keystrokes that I assembled and made available online. That table, which was relatively complete (incomplete might be a more honest appraisal), was more or less current as of Delphi 2009. I had intended to keep this table up-to-date, but like so many casual obligations, intention met reality, and the table has been left untouched, until now.
What motivated the update is that this past December I had the pleasure to cross paths once again with fellow Embacadero MVP and Delphi advocate Brian Long. We were speakers at the annual Sofware Developer Network conference in The Netherlands, were one of Brian's sessions was titled "IDE Productivity Tips." A big part of his presentation covered useful editor keystrokes.
We got talking after his presentation, and I mentioned my negleted key combination table and asked if he'd care to take a look and help bring it up to date. He agreed, but I really don't think either of us could imagine how much work it was going to take. Now, two months later, after many email exchanges, and hours of review and research, we are prepared to share our efforts and publish the updated table of Delphi editor keystrokes and key combinations.
The updated table now includes the many key combinations that have been added in the four releases of Delphi since I published the original table, and boasts a whopping 185 different editor key and key combinations. The new list also includes those editor keystrokes that are active during debugging, which were missing entirely from the original list.
Brian has a keen eye. That, combined with his extensive knowledge of Delphi, significantly improved the scope and consistency of the original table. I am grateful for his effort, and hope that you will appreciate it as well.
Edit: I will make an effort to keep this table updated, and have made several changes since the original 4 February 4 2013 post. The most recent version of this table was updated on 6 February 2013.
You can access the latest version of the updated table using the following link: http://www.JensenDataSystems.com/DelphiEditorKeyTable.pdf
Wednesday, January 23, 2013
Delphi Developer Days 2013 Cities and Dates
Wednesday, November 7, 2012
Delphi Developer Days with Bob Swart and Cary Jensen
It is my special pleasure to announce that Bob Swart is joining me in presenting the annual Delphi Developer Days tours. Bob, known internationally as "Dr.Bob," is one of the world's leading Delphi experts and a dedicated supporter of the Delphi community.
Bob is singularly qualified to fill the vacancy created when Marco Cantú accepted the position of Delphi Product Manager. Like me, Bob is a Delphi developer, consultant, trainer, and author. His blog, "Dr. Bob Examines," has served as an invaluable lifeline to countless Delphi developers, and his Delphi courseware is highly regarded.
Bob and I have known each other for years. He has served as my editor for the articles that I write for the Software Development Network Magazine, and I have worked with him on numerous Delphi conferences. Bob has also been a featured guest speaker at Delphi Developer Days for the past several years, so he is intimately familiar with the format.
I am excited about this new opportunity to work together with Bob Swart on Delphi Developer Days. In the coming months, we will announce our 2013 topics, cities, and dates. We look forward to bringing to you the highest quality Delphi events, and invite your to visit www.DelphiDeveloperDays.com to learn more about these intense, two-day Delphi seminars.
Tuesday, November 6, 2012
Congratulations Marco Cantú, Delphi Product Manager
I am not the first, nor will I be the last, but I want to wish Marco Cantú the very best in his new position as Delphi Product Manager at Embarcadero. This is great news for the Delphi community, and I am excited about what the future will bring.
I've known Marco since we first met in London in the mid-1990s at the annual Borland European conference, and have worked closely with him for more than five years organizing and delivering our annual Delphi Developer Days tours. I can attest to his commitment, knowledge, humor, and insight, and know that he will bring his many gifts to his new role.
As far as Delphi Developer Days is concerned, this is not the end, but rather a transition. I have exciting news to share about DDD, but that will have to wait until tommorow. For now, let me join in the chorus to extend a warm and hearty "congratulations" to both Marco and Embarcadero Technologies. May your future successes be ours as well.
Wednesday, August 22, 2012
Dragging and Dropping into DBGrids
Drag-and-drop operations, where the user drags data from one object and drops it into another object, has been a standard feature in Delphi applications for a long time. Is dragging and dropping with a DBGrid as the target more difficult than, say, a ListBox? Well, that depends.
If you are dragging from some source object and simply dropping that value into a DBGrid, without concern for where the dropped data will appear in the DBGrid, then no. From the DBGrid's OnDragDrop event you read the data from the source object and insert that data into the underlying DataSet. If the DataSet is displaying its data using an index, the dropped data will appear in the grid at the position dictated by the index key, otherwise it will appear at the end of the grid.
It's when you actually want to drop the source data into a particular row of the DBGrid that things become more complicated. It is even more complicated if you want to permit the user to be able to re-order the records in a DBGrid by dragging a record from one position in the DBGrid, dropping it into a new position.
Before I demonstrate how to implement sophisticated drag-and-drop operations using a DBGrid, let's first consider what makes this operation complicated. In a word, indexes. In most databases the order of records in a table is dictated by an index, and in most cases, a primary index. For the same reason, the sequence in which the records appear in a DBGrid are either affected by an index, or in the case of a query, an ORDER BY clause.
What this suggests is that you must manipulate the fields involved in the index or ORDER BY clause of your DataSet when implementing drag-and-drop in a DBGrid, which is correct. Following this logic, you might assume that with every drop operation you are going to write to your database. While you can do that, it is something that I personally want to avoid. Instead, I would prefer to implement the drag-and-drop functions on a cached version of the data, writing to the database only after the user is satisfied with the order of the data they are working with.
So, how is this done? With a ClientDataSet, of course.
Here's the basic approach. Associate a DBGrid with a ClientDataSet that makes use of an integer index that dictates the order of records in the grid. In some cases, including the code sample associated with this post, the ClientDataSet might be empty initially. In other cases, the ClientDataSet is first populated from a database.
When the data is loaded from a database, the field associated with the ordering index is most likely not actually a field in the database. Instead, it is an extra field added for ordering purposes. However, if your underlying data has a field that specifies order, you can use that. In my particular implementation it is necessary that this ordering field begin with 1 (the first record) and be sequential. This is just a detail, however, and the code that I will show could be modified to accommodate any kind of ordinal data.
Once you have a ClientDataSet with these features, all you need to do is to enable drag-and-drop and then implement OnDragOver and OnDragDrop event handlers from which you perform some basic operations. These techniques are demonstrated in the DBGridDragDrop project, which you can download from the preceding link. I wrote this project in Delphi XE2, and tested the posted code in Delphi 7. As a result, I assume that it will run in Delphi 7 Professional and later.
This project demonstrates two especially valuable drag-and-drop techniques. The first is dropping to an arbitrary position in the DBGrid. The second is dragging records from one position in the DBGrid to another.
There are two drag sources in this project, a ListBox and a DBGrid, and one target, the DBGrid. For demonstration purposes, the ListBox is loaded from a call to the PopulateListBox method, shown here:
procedure TForm1.PopulateListBox;
begin
ListBox.Clear;
ListBox.Items.Add('One');
ListBox.Items.Add('Two');
ListBox.Items.Add('Three');
ListBox.Items.Add('Four');
ListBox.Items.Add('Five');
ListBox.Items.Add('Six');
ListBox.Items.Add('Seven');
ListBox.Items.Add('Eight');
ListBox.Items.Add('Nine');
ListBox.Items.Add('Ten');
end;
The ClientDataSet in this project begins as an empty DataSet. This ClientDataSet is created in the following method.
procedure TForm1.CreateClientDataSet;
begin
ClientDataSet := TClientDataSet.Create(Self);
//This example assumes that the field defining the
//order of records is named Sequence. This field
//can appear in any position in the table structure.
ClientDataSet.FieldDefs.Add('Sequence', ftInteger);
ClientDataSet.FieldDefs.Add('Field', ftString, 30);
//The ClientDataSet can have any number of fields
//The following field is just for demonstration
ClientDataSet.FieldDefs.Add('RandomNumber', ftInteger);
ClientDataSet.CreateDataSet;
ClientDataSet.IndexFieldNames := 'Sequence';
DataSource1.DataSet := ClientDataSet;
end;
Both of these methods are called when the main form is first created, producing the form shown in the following figure.
Figure 1. The DBGridDragDrop project main form
We could set the DragMode property of both the ListBox and DBGrid to dmAutomatic, but I like to avoid that. Instead, I prefer to initiate the drag operation only after the mouse has been dragged some reasonable distance. Otherwise, a drag operation begins with the slightest mouse down drag. As a result, I initiate the drag operation from the OnMouseMove event handler of both drag sources. This event handler is shown here.
procedure TForm1.MouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
const //this constant is typically declared with a higher scope
MouseMovePixels = 15;
begin
if ssLeft in Shift then
TListBox(Sender).BeginDrag(False, MouseMovePixels);
end;
We need to add an OnDragOver event handler to the drop target in order to have the mouse cursor change to indicate that dropping is allowed (and to permit the OnDragDrop event handler to trigger). This event handler is shown here, and it is assigned only to the DBGrid.
procedure TForm1.DBGridDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin //Test for acceptable drag origin classes or objects Accept := (Source is TListBox) or (Source is TDBGrid); end;
Finally, the drop operation is performed from the target's OnDragDrop event handler. From here we determine the source of the drag operation (ListBox versus DBGrid) and take the appropriate action. Again, here is the event handler. It is only associated with the DBGrid.
procedure TForm1.DBGridDragDrop(Sender, Source: TObject; X, Y: Integer);
var
GridRow: Integer;
OriginalRow: Integer;
begin
GridRow := DBGrid.MouseCoord(X,Y).Y;
if GridRow = 0 then
GridRow := 1;
if (Source is TListBox) then
begin
//An item is being dropped into the DBGrid
if ClientDataSet.IsEmpty then
begin
//The grid is empty. Add the item in the first position
ClientDataSet.AppendRecord([1,
TListBox(Source).Items[TListBox(Source).ItemIndex],
RandomRange(1, 101)]);
end
else
begin
//Insert the item at the position of the drop
if GridRow = -1 then //the drop is at the end of the DBGrid
GridRow := ClientDataSet.RecordCount + 1
else //the drop needs to be inserted into the DBGrid. Make room
ResequenceCDS(ClientDataSet, GridRow);
//Insert the new item at the drop position
ClientDataSet.InsertRecord(
[GridRow, TListBox(Source).Items[TListBoxSource).ItemIndex],
RandomRange(1, 101)]);
end;
//Remove the dropped item from the source (optional)
TListBox(Source).Items.Delete(TListBox(Source).ItemIndex);
end
else
if Source = Sender then
begin
//We are dragging within the DBGrid
if ClientDataSet.IsEmpty then exit;
OriginalRow := ClientDataSet.RecNo;
if (OriginalRow = GridRow) or (GridRow = -1) then
exit
else
MoveRecord(ClientDataSet, OriginalRow, GridRow);
end;
end;
As you can see, one of the tricks to this technique is getting the row over which the drop operation occurred. This is done using the DBGrid's public MouseCoord method, to which we pass the X and Y coordinates, which we obtain from the event handler's parameters. We then use the Y property of the returned TGridCoord object to discover the record being dropped on. (MouseCoord is public in Delphi 7 and later. If you are using an earlier version where this method is protected, refer to Zarko's THackDBGrid to expose this method to your code.)Dragging and Dropping to a Specific DBGrid Position
What happens next depends on the source of the drag operation. If the drag operation originated from a ListBox, we need to ensure that it is dropped into the proper position within the DBGrid. To do this, we have to check for three possible conditions.If the ClientDataSet is empty, we simply insert a record, assigning a sequence of 1. If the integer Y property of the TGridCoord instance returned by MouseCoord evaluates to -1, the user has dropped at the end of the DBGrid. This, too, is a simple matter of inserting the record with a sequence of TClientDataSet.RecordCount + 1.
It is when the drop occurs somewhere in the middle of the DBGrid that the approach is more complicated. In short, we need to re-sequence all of the records appearing in the drop location, adding 1 to their sequence field value. After that we can insert the record using the drop position record number as the value for the sequence field. In the OnDragDrop event handler listed previously this task is handled first by a call to ResequenceCDS, after which the TClientDataSet's InsertRecord method is invoked.
It is in the ResequenceCDS method that things get fun. As you can see in the code listing that follows, a ClientDataSet cloned cursor is used to perform the record resequencing. This has the effect of changing the underlying indexed field values with a minimal impact on the user interface.
procedure TForm1.ResequenceCDS(cds: TClientDataSet; FromRow: Integer);
var
clone: TClientDataSet;
SequenceFld: TField;
begin
clone := TClientDataSet.Create(nil);
try
clone.CloneCursor(cds, True);
SequenceFld := clone.FieldByName('Sequence');
begin
//Shift all records down to make room in the sequence
//for the record being inserted
clone.Last;
while (SequenceFld.AsInteger >= FromRow) and not clone.bof do
begin
clone.Edit;
SequenceFld.AsInteger := SequenceFld.AsInteger + 1;
clone.Post;
clone.Prior;
end;
end
finally
clone.Free;
end;
end;
The following three figures demonstrate this drag-and-drop in action. Figure 2 depicts the DBGrid after three records have been added, while Figure 3 shows a fourth record being dropped into the second position of the DBGrid. Figure 4 shows how the DBGrid appears following the drop operation.
Figure 2. Three records have been dropped onto the DBGrid from the ListBox.
Figure 3. A value from the ListBox is being dropped into the second position of the DBGrid
Figure 4. The drag-and-drop operation has dropped the new value in position 2
Dragging and Dropping Existing Records in a DBGrid
Dragging an existing record from its current position to a new position within the DBGrid is a bit more involved. This process is handled by the MoveRecord method shown here.procedure TForm1.MoveRecord(cds: TClientDataSet; OldPos, NewPos: Integer);
var
clone: TClientDataSet;
SequenceFld: TField;
begin
clone := TClientDataSet.Create(nil);
try
clone.CloneCursor(cds, True);
SequenceFld := clone.FieldByName('Sequence');
clone.RecNo := OldPos;
clone.Edit;
//Move the record being moved to the end of the sequence
SequenceFld.AsInteger := cds.RecordCount + 1;
clone.Post;
if OldPos < NewPos then
begin
//Shift the records after the original old position up one position
clone.RecNo := OldPos;
while (clone.RecNo < NewPos) do
begin
clone.Edit;
SequenceFld.AsInteger := SequenceFld.AsInteger - 1;
clone.Post;
clone.Next;
end;
end
else
begin
//Shift the record before the original position down one position
clone.IndexFieldNames := 'Sequence';
clone.RecNo := OldPos - 1;
while (clone.RecNo >= NewPos) and (not clone.bof) do
begin
clone.Edit;
SequenceFld.AsInteger := SequenceFld.AsInteger + 1;
clone.Post;
clone.Prior;
end;
end;
//Move the record being moved to its new position
clone.RecNo := cds.RecordCount;
clone.Edit;
SequenceFld.AsInteger := NewPos;
clone.Post;
finally
clone.Free;
end;
cds.RecNo := NewPos;
end;
As you can see from this code, we begin by repositioning the record that is being moved to the end of the index order, assigning to it RecordCount + 1 (in reality this could be a any value outside the current range of sequence numbers, just so long as we get the record we are moving out of the way. Next, depending on whether the move is upwards or downwards, we resequence each of the records that need to be repositioned before we perform the final movement. Finally, we reposition the record we are moving into its new position.
This operation is demonstrated in the following two figures. In Figure 5 the record at position 4 is being dragged to position 2. Figure 6 shows the DBGrid after the drop operation has been completed.
Figure 5. The record in position 4 is being dragged to position 2.
Figure 6. The dragged record has now been moved to position 2.
Deleting Records
We also have to update the sequence when a record is removed. Since the ReadOnly property is set to True on the DBGrid, record removal is something that we do from a custom popup menu. This popup menu has a single item, whose caption is Remove. This menu is surpressed when there are no records in the grid by the OnPopup event handler, shown here.procedure TForm1.Remove1Click(Sender: TObject); var cds: TClientDataSet; begin cds := TClientDataSet(DBGrid.DataSource.DataSet); RemoveFromSequence(cds, cds.RecNo); end;
Removal is performed by the RemoveFromSequence method, shown here.
procedure TForm1.RemoveFromSequence(cds: TClientDataSet; Position: Integer);
var
clone: TClientDataSet;
SeqFld: TField;
begin
clone := TClientDataSet.Create(nil);
try
clone.CloneCursor(cds, True);
SeqFld := clone.FieldByName('Sequence');
clone.RecNo := Position;
// Edit - Added this to next line: and (clone.RecordCount = 1)
if (Position = 1) and (clone.RecordCount = 1) then
begin
//There is just one record. Delete it,
//but do not try to set a new record position
clone.Delete;
end
else
begin
if clone.RecNo = clone.RecordCount then
begin
clone.Delete;
cds.RecNo := cds.RecordCount;
end
else
begin
clone.Delete;
while not clone.eof do
begin
clone.Edit;
SeqFld.AsInteger := SeqFld.AsInteger - 1;
clone.Post;
clone.Next;
end;
cds.RecNo := Position;
end;
end;
finally
clone.Free;
end;
end;
To Clone or Not To Clone
All of the methods that I have shown here make use of cloned cursors, which are very handy for working with a ClientDataSet in the background. However, cloned cursors are not essential for these techniques to work. In fact, the code sample in the download for this post includes two versions of the three workhorse methods (ResequenceCDS, MoveRecord, and RemoveFromSequence). The version that I have shown here that employes cloned cursors, and another version that does not. You might find that one version works better than the other, depending on the circumstances of your data.Summary
Dragging and Dropping is a common feature in Delphi applications, and a great technique for productive user interface design. Dragging and dropping within DBGrids, however, is more involved, especially if you want to drop somewhere other than the end of the DBGrid. In this posting, I have demonstrated how you can use a ClientDataSet and a cloned ClientDataSet cursor to implement sophisticated drag-and-drop operations involving a DBGrid as a target.If you want to learn more about ClientDataSets, including an entire chapter on cloned cursors, please take a look at my most recent book Delphi in Depth: ClientDataSets.
Copyright © 2012 Cary Jensen. All Rights Reserved.
Monday, July 2, 2012
Getting a Lock on Delphi
Some of Delphi's more powerful and best kept secrets have actually been in the product since the beginning, and in this short post I want to point out one of these, a feature that most Delphi developers I talk to are unaware of.
Last October I gave a keynote address in Düsseldorf, Germany, as part of the 15th annual Delphi Entwickler Konferenz (Developer Conference), generally known as EKON. The talk was titled The Evolution of Delphi: From Delphi to Delphi XE2, and I used the opportunity to reflect on the many groundbreaking innovations introduced in Delphi throughout the years, with one of the biggest innovations being Delphi itself.
Early in the talk I demonstrated the original version of Delphi (which was called Delphi, not Delphi 1, but the name Delphi 1 certainly makes it easier to convey what we are talking about when referring to the first release of this product). Seeing that original 16-bit application run again, albeit in 32-bit Windows 7, was fun and educational. (I had tried to create a virtual machine with Windows 3.11 for Workgroups, but alas, I tossed those disks during my last move.) Yes, Delphi 1 looks funny compared to the sleek and sophisticated stylings of the current product, but it really did have an amazing collection of capabilities.
Among the more profound features was runtime type information, a mechanism for generically examining the published members of objects, determining their names and types, and reading and writing their values. It was groundbreaking, and served as the foundation for the designer that changed the way we built our user interfaces.
But that is not the feature I want to talk about here. Instead, I want to bring attention to a often overlooked and greatly underused feature that can go a long way to making your life easier, at least as far as user interface design is concerned. This feature is a menu item called Lock Controls, and you can see it in the following figure from Delphi's (the original) Edit menu.
Lock Controls is a toggle menu item. Turn it on by select Edit | Lock Controls, and turn it off by selecting it again. When Lock Controls is on, you cannot change the positions of the components in the designer. Turn it off, and you can once again move and resize your controls.
Lock Controls is exceptionally valuable when you have a complex user interface with many controls precisely positioned, and you do not want to accidentally move anything as you select those components to adjust their properties or add event handlers. Once you have the components positioned where you need them, select Edit | Lock Controls to freeze the user interface. Now you can select individual components or groups of components without worrying about unintentionally moving them with your mouse.
Lock Controls applies to all forms, data modules, and frames, not just the one you are currently working on. As a result, you will need to turn it back off before you return to the task of user interface design.
Here is how Lock Controls looks today, in Delphi XE2. It's still in the same place, but works about the same as it did in the beginning. Actually, Lock Controls now prevents any change in position or size of controls. In pre-Delphi 2005 versions, you could still move or resize controls using your keyboard with the Ctrl and arrow keys. Nowadays, locking controls even prevents those operations.


