Whilst discussing the DOCU program last week, the Occupational Psychologist asked why she can't store screenshots along with the written documentation. I wasn't about to tell her that the program was implemented with a RichEdit component, which implies very strongly that only rich text can be saved, so I said that I would consider the options.
The simple part of the option which I found was to add a field in the database of 'blob' type; such a field can store anything as a sequence of bytes. Theoretically I know how to display jpg images stored in a database as one of our programs displays images, and even more theoretically I know how to save such an image into the database.
The solution actually required several items
- adding a blob field to the database
- learning how to save an image from the clipboard to a picture component in Delphi
- saving that picture to the database
- clearing a picture from the database
- loading the contents of a blob field to a picture
- creating a suitable form from the 'entry' form and closing it appropriately
From the outset, I decided that each entry in the database could have only one picture assigned to it; otherwise this would add complexity (Should the user choose which picture to display? Should all pictures be displayed for this entry? How to manage the pictures?). Thus I was able to add one icon to the toolbar (see yesterday's blog) instead of having to add an image choosing dialog.
My logic was as follows: if the entry has an assigned picture, then open a son form and display the picture. Otherwise, open a son form, copy into it whatever graphic is in the clipboard's memory, display it and save it to the database. There also should be an option to clear the picture in case a wrong graphic was saved.
Here's the code which does all of the above.
Procedure TDoPicture.Execute (const s: string; aleft, atop: longint);
var
empty: boolean;
bmp: TBitmap;
ms: TMemoryStream;
j: TJPEGImage;
begin
caption:= s;
left:= aleft;
top:= atop;
with qGetPicture do
begin
params[0].asinteger:= myentry;
open;
empty:= isempty;
if not empty then
begin
j:= TJPEGImage.Create;
try
j.Assign (qGetPicturePIC);
OnAssign (j)
finally
j.Free;
end;
end;
close
end;
if empty and Clipboard.HasFormat (CF_PICTURE) then
begin
bmp:= TBitmap.Create;
j:= TJPEGImage.Create;
try
bmp.Assign (Clipboard);
j.Assign (bmp);
OnAssign (j);
finally
bmp.Free;
j.free
end;
ms:= TMemoryStream.Create;
try
Image1.Picture.Graphic.SaveToStream (ms);
ms.Position:= 0;
with qSavePicture do
begin
parambyname ('p1').asinteger:= myentry;
ParamByName('p2').LoadFromStream (ms, ftGraphic);
execsql
end;
finally
ms.Free;
end
end;
show
end;
procedure TDoPicture.OnAssign (j: TJPEGImage);
begin
Image1.picture.assign (j);
image1.autosize:= true;
// resize the form to be slightly larger than the stored image
width:= image1.Width + 16;
height:= image1.Height + 48;
end;
Looking at the above now, it all seems so obvious, but it wasn't when I was writing the program! Taking it bit by bit:
- the first few lines are concerned with setting up the form with the correct caption; the form is placed at the same height and to the right of the calling form
- a query is then opened which checks whether there is an assigned picture. If the query is empty, then the 'empty' variable becomes true. If not empty, then the next few lines show how to read a blob from the database, store it in a temporary variable and then cause the blob to be displayed as a jpg image. The 'OnAssign' procedure resizes the form to be slightly larger than the image.
- If there is no assigned picture, then the program checks to see whether the clipboard holds graphic data. If so, the image is first saved in a temporary bitmap (bmp) and then converted into jpg format, which is then displayed. Afterwards, the image is saved to the database, an action which requires the use of a stream.
As I wanted to have the user interface as simple as possible (ie no user interface at all), I was stumped at first how I was going to allow the user to remove the stored graphic. Then I remembered that I have been using the system menu frequently for such actions; the code became very simple to write.
At the beginning of the form, the following adds an option to the system menu
const
SC_Original = WM_USER + 2;
procedure TDoPicture.FormCreate (Sender: TObject);
var
SysMenu: HMenu;
begin
SysMenu:= GetSystemMenu (Handle, FALSE);
AppendMenu (SysMenu, MF_STRING, SC_Original, 'Clear picture');
end;
And this is how a click on that menu option is handled
procedure TDoPicture.WMSysCommand (var Msg: TWMSysCommand);
begin
if Msg.CmdType = SC_Original then
begin
qClear.params[0].asinteger:= myentry;
qClear.ExecSQL;
image1.picture:= nil;
close
end
else inherited;
end;
In an MDI application, the program 'knows' about its child forms because they are 'stored' in the MDIChildren array. As DOCU is not an MDI program, another solution is required; Delphi stores all on screen forms in the 'forms' property of the 'screen' variable. So closing the picture form when its parent form is closed requires the following code
for i:= 1 to screen.FormCount do
if screen.forms[i-1] is TDoPicture
then if TDoPicture (screen.forms[i-1]).myentry = entry
then screen.forms[i-1].close;
Here's a pretty useless screenshot of an entry form along with its graphic; what would seem to be WinAmp playing in a window is actually a screenshot of WinAmp captured and stored in Docu.