2010. október 31., vasárnap
How to create and use a resource-only DLL
Problem/Question/Abstract:
How do I create a DLL with just graphics files, then allow several different DLL's and applications to use these files?
Answer:
First, create a resource source file (*.RC) containing references to the bitmaps:
CLOSEBUTTON BITMAP "C:\Projects\closebtn.bmp"
OPENBUTTON BITMAP "C:\Projects\openbtn.bmp"
This is just an ordinary text file, so you can use the Delphi code editor or any other text editor to create it. Make sure the names of the bitmaps (CLOSEBUTTON, etc.) are ALL UPPERCASE.
Next, compile this .RC file to create the corresponding .RES file, using BRCC32.EXE:
brcc32 myimages.rc
Now start a new DLL project in Delphi and link the .RES file into it with an $R directive:
library MyImages;
uses
Windows;
{$R MYIMAGES.RES}
begin
end.
Compile this code, and you now have a resource DLL containing the bitmaps.
To use these resources in an EXE or another DLL, you need to use LoadLibrary to get a handle to the DLL, and then LoadBitmap to get a handle to the bitmap:
var
DllHandle: THandle;
CloseButtonBmp: TBitmap;
OpenButtonBmp: TBitmap;
begin
DllHandle := LoadLibrary('MyImages.dll');
if DllHandle <> 0 then
try
CloseButtonBmp := TBitmap.Create;
CloseButtonBmp.Handle := LoadBitmap(DllHandle, 'CLOSEBUTTON');
OpenButtonBmp := TBitmap.Create;
OpenButtonBmp.Handle := LoadBitmap(DllHandle, 'OPENBUTTON');
{...}
finally
FreeLibrary(DllHandle)
end;
else
ShowMessage(SysErrorMessage(GetLastError))
end;
Once you've loaded the bitmaps, you can assign them to their final destinations, wherever that may be, and then free them.
2010. október 30., szombat
Advanced Options
Problem/Question/Abstract:
Some Advanced Options that I found on the Internet.
Answer:
1) It scroll automatically the Delphi Palette
[HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\Extras]
"AutoPaletteSelect"="1"
2) It scroll automatically the components on Palette
[HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\Extras]
"AutoPaletteScroll"="1"
3) It show font names in Object Inspector
[HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\Extras]
"FontNamePropertyDisplayFontNames"="1"
4) It show compiling errors in message view window
[HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\Compiling]
"ShowCodeInsiteError"="1"
5) Default fonts for new forms
[HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\FormDesign]
"DefaultFont"="Tahoma, 8, Normal"
2010. október 29., péntek
How to hide MDI child forms
Problem/Question/Abstract:
How to hide MDI child forms
Answer:
To hide:
{ ... }
if Form2.WindowState = wsMaximized then
Form2.WindowState := wsNormal;
ShowWindow(Form2.Handle, SW_Hide);
{ ... }
To redisplay:
{ ... }
SetWindowPos(Form2.Handle, HWND_TOP, 0, 0, 0, 0, SWP_NoMove or
SWP_NoSize or SWP_ShowWindow);
WinProcs.SetFocus(Form2.Handle);
{ ... }
2010. október 27., szerda
Only numerical input in a TEdit
Problem/Question/Abstract:
Only numerical input in a TEdit
Answer:
If you want to limit the input of a TEdit to numerical strings only, you can discard the "invalid" characters in its OnKeyPress event handler.
Let's assume that you only want to allow positive integer numbers. The code for the OnKeyPress event handler is as follows:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
// #8 is Backspace
if not (Key in [#8, '0'..'9']) then
begin
ShowMessage('Invalid key');
// Discard the key
Key := #0;
end;
end;
If you also want numbers with a decimal fraction, you must allow a POINT or a COMMA, but only once. For an international version that looks at the correct decimal separator, the code could be as follows:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if not (Key in [#8, '0'..'9', DecimalSeparator]) then
begin
ShowMessage('Invalid key: ' + Key);
Key := #0;
end
else if (Key = DecimalSeparator) and
(Pos(Key, Edit1.Text) > 0) then
begin
ShowMessage('Invalid Key: twice ' + Key);
Key := #0;
end;
end;
And here's a full blown version of the event handler, accepting a decimal separator and negative numbers (minus sign: only accepted once, has to be the first character):
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if not (Key in [#8, '0'..'9', '-', DecimalSeparator]) then
begin
ShowMessage('Invalid key: ' + Key);
Key := #0;
end
else if ((Key = DecimalSeparator) or (Key = '-')) and
(Pos(Key, Edit1.Text) > 0) then
begin
ShowMessage('Invalid Key: twice ' + Key);
Key := #0;
end
else if (Key = '-') and
(Edit1.SelStart <> 0) then
begin
ShowMessage('Only allowed at beginning of number: ' + Key);
Key := #0;
end;
end;
How about giving that same behaviour to several TEdits on the same form, say 10 of them? In the Object Inspector, you change the name of the event handler of Edit1 from Edit1KeyPress to Edit1_10KeyPress or something similar. Delphi automatically changes the name in the code editor, don't worry.
Then, for each next TEdit, you select its OnKeyPress event and you select Edit1_10KeyPress from the listbox next to the event.
Finally, we have to slightly adapt the code. Intead of pointing to Edit1, we must point to "the TEdit that generated the event", in other words: the edit-box where the cursor was when a key was depressed. When you look at the template for the event handler that Delphi made, you see the parameter Sender: that's a pointer to the component that generated the event. But we are not allowed to use it straight away in our code, we must specify that we're dealing with a component of the type TEdit. That's done with the code Sender as TEdit:
procedure TForm1.Edit1_10KeyPress(Sender: TObject; var Key: Char);
begin
if not (Key in [#8, '0'..'9', '-', DecimalSeparator]) then
begin
ShowMessage('Invalid key: ' + Key);
Key := #0;
end
else if ((Key = DecimalSeparator) or (Key = '-')) and
(Pos(Key, (Sender as TEdit).Text) > 0) then
begin
ShowMessage('Invalid Key: twice ' + Key);
Key := #0;
end
else if (Key = '-') and
((Sender as TEdit).SelStart <> 0) then
begin
ShowMessage('Only allowed at beginning of number: ' + Key);
Key := #0;
end;
end;
2010. október 26., kedd
Draw a filled circle using ScanLine
Problem/Question/Abstract:
I am looking for some code to draw a filled circle on a bitmap or change colors of pixels within a circle on it, using Bitmap.Scanline. Any suggestions or ideas on how to do this, the edges need to be perfect and it has to be fast.
Answer:
Perfect edges mean you will have to work with an alpha channel and do anti-aliasing. This means, that you either have to use a 32-bit bitmap (see e.g. Graphics32) or you have to first draw the background image in the bitmap and directly blend it when rendering the circle. Next question: do you want to use integer precision or floating point precision for the circle properties like center point and diameter? If you use integer, you only have to draw 1/8 of the circle and the rest can be copied/mirrored/flipped around. Assuming floating point, and a grayscale bitmap, here's an approach:
CX, CY: center of circle (single)
R: radius of circle (single)
F: feather size (the number of pixels used as blend area, usually 1 pix) (single)
Determine bounds in Y (integers):
LX := floor(CX - R - F * 0.5);
RX := ceil(CX + R + F * 0.5);
LY := floor(CY - R - F * 0.5);
RY := ceil(CY + R + F * 0.5);
Determine some helpful values (singles)
RPF2 = sqr(R + F/2);
RMF2 = sqr(R - F/2);
{ ... }
var
P: PByteArray
sqdist: single;
{ ... }
{Loop through Y values}
{for y := LY to RY do begin -> not very safe}
for y := max(LY, 0) to Min(RY, Bitmap.Height - 1) do
P := Bitmap.Scanline[y];
{Loop through X values}
for x := Max(LX, 0) to Min(RX, Bitmap.Width - 1) do
begin
{Determine squared distance from center for this pixel}
sqdist := sqr(y - CY) + sqr(x - CX); {Or use hypot() function}
{Inside outer circle?}
if sqdist < RPF2 then
begin
{Inside inner circle?}
if sqdist < RMF1 then
{Inside the inner circle.. just give the scanline the new color}
P[x] := 255
else
begin
{We are inbetween the inner and outer bound, now mix the color}
Fact := Max(0, Min(255, round(((R - sqrt(sqdist)) * 2 / F) * 128 + 128)));
P[x] := (255 - Fact) * P[x] + Fact;
end;
end;
{ ... }
This algorithm is optimized a bit but could be made faster probably. Untested!
2010. október 25., hétfő
Copy a menu item from a TMainMenu to an empty popup menu
Problem/Question/Abstract:
How to copy a menu item from a TMainMenu to an empty popup menu
Answer:
This will only copy the first level of menu items:
procedure TForm1.PopupMenu1Popup(Sender: TObject);
var
I: Integer;
MenuItem: TMenuItem;
begin
TPopupMenu(Sender).Items.Clear;
{Copy menu items from first mainmenu (File1)}
for I := 0 to File1.Count - 1 do
begin
with File1.Items[I] do
MenuItem := NewItem(Caption, ShortCut, Checked, Enabled, OnClick, HelpContext,
Name);
TPopupMenu(Sender).Items.Add(MenuItem);
end;
end;
How to copy a menu item from a TMainMenu to an empty popup menu
Answer:
This will only copy the first level of menu items:
procedure TForm1.PopupMenu1Popup(Sender: TObject);
var
I: Integer;
MenuItem: TMenuItem;
begin
TPopupMenu(Sender).Items.Clear;
{Copy menu items from first mainmenu (File1)}
for I := 0 to File1.Count - 1 do
begin
with File1.Items[I] do
MenuItem := NewItem(Caption, ShortCut, Checked, Enabled, OnClick, HelpContext,
Name);
TPopupMenu(Sender).Items.Add(MenuItem);
end;
end;
2010. október 24., vasárnap
Convert a string to a set and vice versa using RTTI
Problem/Question/Abstract:
Given a string like '[mbOk, mbCancel]', what is a simple way to use the routines in TypInfo to produce the corresponding set?
Answer:
uses
TypInfo;
function ButtonStringToSet(const S: string): TMsgDlgButtons;
var
Temp: TStringlist;
I: Integer;
N1, N2: Integer;
begin
Result := [];
N1 := Pos('[', S);
N2 := Pos(']', S);
if N2 = 0 then
N2 := Length(S) + 1;
Assert(N2 > N1);
Temp := TStringlist.Create;
try
Temp.Commatext := Copy(S, N1 + 1, N2 - N1 - 1);
for i := 0 to Temp.Count - 1 do
Include(Result, TMsgDlgBtn(TypInfo.GetEnumValue(TypeInfo(TMsgDlgBtn),
Trim(Temp[I]))));
finally
Temp.Free;
end;
end;
function SetToButtonString(Buttons: TMsgDlgButtons): string;
var
Temp: TStringlist;
Btn: TMsgDlgBtn;
begin
Temp := TStringlist.Create;
try
for Btn := Low(Btn) to High(Btn) do
if Btn in Buttons then
Temp.Add(TypInfo.GetEnumName(TypeInfo(TMsgDlgBtn), Ord(Btn)));
Result := Format('[%s]', [Temp.Commatext]);
finally
Temp.Free;
end;
end;
Given a string like '[mbOk, mbCancel]', what is a simple way to use the routines in TypInfo to produce the corresponding set?
Answer:
uses
TypInfo;
function ButtonStringToSet(const S: string): TMsgDlgButtons;
var
Temp: TStringlist;
I: Integer;
N1, N2: Integer;
begin
Result := [];
N1 := Pos('[', S);
N2 := Pos(']', S);
if N2 = 0 then
N2 := Length(S) + 1;
Assert(N2 > N1);
Temp := TStringlist.Create;
try
Temp.Commatext := Copy(S, N1 + 1, N2 - N1 - 1);
for i := 0 to Temp.Count - 1 do
Include(Result, TMsgDlgBtn(TypInfo.GetEnumValue(TypeInfo(TMsgDlgBtn),
Trim(Temp[I]))));
finally
Temp.Free;
end;
end;
function SetToButtonString(Buttons: TMsgDlgButtons): string;
var
Temp: TStringlist;
Btn: TMsgDlgBtn;
begin
Temp := TStringlist.Create;
try
for Btn := Low(Btn) to High(Btn) do
if Btn in Buttons then
Temp.Add(TypInfo.GetEnumName(TypeInfo(TMsgDlgBtn), Ord(Btn)));
Result := Format('[%s]', [Temp.Commatext]);
finally
Temp.Free;
end;
end;
2010. október 23., szombat
Accessing hidden properties
Problem/Question/Abstract:
How can I access the InplaceEditor property of a Grid?
Answer:
Some components have useful properties, but for some reason they were declared in their protected section, so they are not readily available to the programmer. For example, TStringGrid, TDrawGrid, TDBGrid and in general any descendant of TCustomGrid has an InplaceEditor property that represents the text edit box used for editing cell values. However you can't access this property directly because it has been declared as protected.
The easiest workaround to this problem is subclassing (deriving) your component with the only purpose or publishing the protected property. For example:
type
TDBGridX = class(TDBGrid)
public
property InplaceEditor;
end;
We don't need to intall this new component and register it in the components palette (which I consider too much of a bother for such a little thing). Instead, any time we want to access this property, we can just cast the object (for example DBGrid1) to our new class. For example:
TDBGridX(DBGrid1).InplaceEditor.SelectAll;
Note: InplaceEditor will be Nil until the first time EditorMode is set to True (either by code or when the user presses F2).
But use the protected property always cause some fault unexpectable.Sush as the Fixcols property. How to resolve it?
The properties were left protected for some reason, usually this being the fact that they are not safe to use directly. There are some limitations when accessing protected fields, properties or methods and normally these limitations are documented (they are not so much "unexpectable").
For example, about the inplace editor the documentation says:
"The inplace editor is created the first time the grid is put in edit mode."
This means that before the first time the grid is put in edit mode, a code like the following will certainly generate an
Access Violation:
TDBGridX(DBGrid1).InplaceEditor.SelectAll;
You can solve this problem by first checking if InplaceEditor is not Nil:
if TDBGridX(DBGrid1).InplaceEditor <> nil then
TDBGridX(DBGrid1).InplaceEditor.SelectAll;
About FixedCols, the documentation says:
"Grids must include at least one scrolling column. Do not set FixedCols to a value greater than ColCount - 1."
This means that for instance the following code will raise an EInvalidGridOperation exception if ColCount <= 2:
TDBGridX(DBGrid1).FixedCols := 2;
For example if you create columns automatically from a Dataset associated with a DataSource, then you should first open the Dataset to let the columns be created, and only then you can set the FixedCols property. For example:
Table1.Active := True;
TDBGridX(DBGrid1).FixedCols := 2;
In conclusion, you should check the documentation first before using the protected properties since normally they have some limitation. As I've shown, the way to circunvent it depends on the case and there is no general rule. There may be also some undocumented problems and side effects and when they appear generally you should check the source code of the component to get an idea of how to avoid or fix them.
Copyright (c) 2001 Ernesto De Spirito
Visit: http://www.latiumsoftware.com/delphi-newsletter.php
2010. október 22., péntek
Add items to the Windows Explorer right-click menu (2)
Problem/Question/Abstract:
Does anybody know how to write a Delphi program that can add itself to the Windows Explorer right-click menu? I have seen some simple cases like adding NotePad for txt files but that only works on one file (if you highlight many files then many instances of Notepad will be created). I want to be able to highlight a group of files and then pass all of them (probably through a command line argument) to my progam so it can act on the group of them.
Answer:
Implement IContextMenu and IShellExtInit:
TOFCContextMenu = class(TComObject, IContextMenu, IShellExtInit)
private
FileList: TStringList;
protected
function IShellExtInit.Initialize = IShellExtInit_Initialize;
function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast,
uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT; pszName: LPSTR;
cchMax: UINT): HResult; stdcall;
function IShellExtInit_Initialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult; stdcall;
public
destructor Destroy; override;
end;
In the Initialize method of the IShellExtInit interface you can determine which files are selected:
function TOFCContextMenu.IShellExtInit_Initialize(pidlFolder: PItemIDList;
lpdobj: IDataObject; hKeyProgID: HKEY): HResult; stdcall;
var
StgMedium: TStgMedium;
FormatEtc: TFormatEtc;
szFile: array[0..MAX_PATH + 1] of Char;
FileCount: Integer;
FileCounter: Integer;
begin
try
if (lpdobj <> nil) then
begin
with FormatEtc do
begin
cfFormat := CF_HDROP;
ptd := nil;
dwAspect := DVASPECT_CONTENT;
lindex := -1;
tymed := TYMED_HGLOBAL;
end;
Result := lpdobj.GetData(FormatEtc, StgMedium);
if (not Failed(Result)) then
begin
FileList := TStringList.Create;
FileList.Clear;
FileList.Sorted := True;
FileList.Duplicates := dupIgnore;
FileCount := DragQueryFile(stgmedium.hGlobal, $FFFFFFFF, nil, 0);
for FileCounter := 0 to FileCount - 1 do
begin
DragQueryFile(stgmedium.hGlobal, FileCounter, szFile, SizeOf(szFile));
FileList.Add(StrPas(szFile));
end;
Result := NOERROR;
ReleaseStgMedium(StgMedium);
end;
end
else
Result := E_INVALIDARG;
except
Result := E_FAIL;
end;
end;
The file list must be freed in the destructor:
destructor TOFCContextMenu.Destroy;
begin
try
FileList.Free;
except
end;
inherited Destroy;
end;
Now implement the other methods:
QueryContextMenu
InvokeCommand
GetCommandString
At the end of the unit you can register the extension:
initialization
TRegisterContextMenuFactory.Create(ComServer, TOFCContextMenu, Class_OFCContextMenu,
'OFCContextMenu', 'A description', ciMultiInstance, tmApartment);
Remember to protect every method with try..except or try..finally. The main application is the explorer. It doesn't support exception handling like a delphi application does. An exception outside a try..except/finally compound causes the explorer to crash.
The TRegisterContextMenuFactory object looks something like this:
type
TRegisterContextMenuFactory = class(TComObjectFactory)
protected
function GetProgID: string; override;
public
procedure UpdateRegistry(Register: Boolean); override;
end;
function TRegisterContextMenuFactory.GetProgID: string;
begin
Result := '';
end;
procedure TRegisterContextMenuFactory.UpdateRegistry(Register: Boolean);
const
ApproveKey = 'SOFTWARE\Microsoft\Windows\CurrentVersion\ShellExtensions\Approved\';
var
ClsID: string;
begin
inherited UpdateRegistry(Register);
ClsID := GUIDToString(ClassID);
if (Register) then
try
{Additional registry settings }
if Win32Platform = VER_PLATFORM_WIN32_NT then
CreateRegKeyEx(ApproveKey, ClsId, PChar(Description), REG_SZ, Length(Description) + 1, HKEY_LOCAL_MACHINE);
except
end
else
try
if Win32Platform = VER_PLATFORM_WIN32_NT then
DeleteRegValue(ApproveKey, ClsId, HKEY_LOCAL_MACHINE);
{Delete additional registry settings }
except
end;
end;
Instead of {Additional registry settings } you must add the registry keys for the extension. Like which file exctension is associated. You can use HKEY_LOCAL_MACHINE\* for all extensions.
2010. október 21., csütörtök
Debug a component at design time (in the IDE)
Problem/Question/Abstract:
Debug a component at design time (in the IDE)
Answer:
To debug a component at design time, follow these steps:
In Delphi go to Tools/Options then go to the "Library" page. Check the "Compile With Debug Info" box.
Rebuild the library.
Run Delphi from within Turbo Debugger.
Use "File/Change Dir" to include the source directories.
2010. október 20., szerda
Fixing a broken generator (InterBase)
Problem/Question/Abstract:
Recently I got unique key violations during insert attempts on a piece of code that used to work (what can go bad, will go bad). I found that the offending field - was actually created by a generator. For some reason the generator returned values that where already in the database.
how can I display the current value of the generator?
how can I adjust the value of the generator?
Answer:
See the example (table name is SD_LOAD, generator name is GEN_SD_LOAD).
Note:
You cannot modify the value of the generator inside of a trigger or stored procedure. You only can call the gen_id() function to increment the value in a generator. The SET GENERATOR command will only work outside of a stored procedure or trigger.
SELECT DISTINCT(GEN_ID(gen_sd_load, 0))FROM sd_load
set GENERATOR gen_sd_load to 2021819
2010. október 19., kedd
How to get Windows uptime?
Problem/Question/Abstract:
How to get Windows uptime?
Answer:
Use the following function:
function UpTime: string;
const
ticksperday: integer = 1000 * 60 * 60 * 24;
ticksperhour: integer = 1000 * 60 * 60;
ticksperminute: integer = 1000 * 60;
tickspersecond: integer = 1000;
var
t: longword;
d, h, m, s: integer;
begin
t := GetTickCount;
d := t div ticksperday;
dec(t, d * ticksperday);
h := t div ticksperhour;
dec(t, h * ticksperhour);
m := t div ticksperminute;
dec(t, m * ticksperminute);
s := t div tickspersecond;
Result := 'Uptime: ' + IntToStr(d) + ' Days ' + IntToStr(h) + ' Hours ' + IntToStr(m)
+ ' Minutes ' + IntToStr(s) + ' Seconds';
end;
2010. október 18., hétfő
Replace the default scrollbar of a TStringGrid with buttons
Problem/Question/Abstract:
I want to put two buttons as a scrollbar to my grid instead of using the default Delphi windows scrollbar. I suppose that I should handle the messages like WM_VSCROLL and WM_HSCROLL and setting my grid.Scrollbars := none
Answer:
You should send these messages to the grid on your button presses, e.g. for a line up:
{ ... }
with stringgrid1 do
begin
perform(WM_VSCROLL, SB_LINEUP, 0);
perform(WM_VSCROLL, SB_ENDSCROLL, 0);
end;
or use:
procedure buttonupClick(sender);
begin
SendMessage(Grid1.Handle, WM_VSCROLL, SB_LINEUP, 0)
end;
2010. október 17., vasárnap
Sparse array implementation using TStringlist
Problem/Question/Abstract:
Sparse arrays are arrays that only uses memory for the cells that are actually in use although the full size of the array is always available. A prime example is the cells in a spreadsheet application: they can have enormous dimensions (like 99999 * 99999 cells) but still only use memory equivalent to the cells where there is any data. This article shows how you can easily create a sparse array with any number of dimensions and of arbitrary size.
Answer:
One solution is to create a new class (let's call it TSparseArray) that stores the data in a TStringlists Objects array and the dimensions in the Strings array as a compound string. Here's a two-dimensional example:
interface
type
TSparseArray = class(TObject)
private
FCells: TStringlist;
function GetCell(Col, Row: integer): TObject;
procedure SetCell(Col, Row: integer; const Value: TObject);
public
constructor Create;
destructor Destroy; override;
property Cells[Col, Row: integer]: TObject read GetCell write SetCell; default;
end;
implementation
const
cFmtDims = '%d:%d';
constructor TSparseArray.Create;
begin
inherited Create;
FCells := TStringlist.Create;
FCells.Sorted := true; // faster retrieval, slower updates, needed for dupIgnore
FCells.Duplicates := dupIgnore;
end;
destructor TSparseArray.Destroy;
begin
FCells.Free;
inherited Destroy;
end;
function TSparseArray.GetCell(Col, Row: integer): TObject;
var
i: integer;
begin
Result := nil;
i := FCells.IndexOf(Format(cFmtDims, [Col, Row]));
if i > -1 then
Result := FCells.Objects[i];
end;
procedure TSparseArray.SetCell(Col, Row: integer; const Value: TObject);
begin
// dupIgnore guarantees that if this cell already exists, this will overwrite it
FCells.AddObject(Format(cFmtDims, [Col, Row]), Value);
end;
end.
To create a sparse array with more dimensions, you just have to redefine the Cells property (and the read / write methods) and change the format of cFmtDims accordingly. You can even mix dimensions of different types (i.e Cells[const Row:string;Col:integer]:TObject). I think you can come up with more neat things yourself. Enjoy!
2010. október 16., szombat
Accessing DataBase via 3th server
Problem/Question/Abstract:
Writing n-tier Application for accessing client's app to DataBase without installing Client of Database via 3th server using Indy
Answer:
This is simple sample how organize work client's app with DataBase without installing Database client or organize remote access to database.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, IdTCPServer, IdBaseComponent, IdComponent,
IdTCPConnection, IdTCPClient, DBClient, Provider, Grids, DBGrids, DB,
OracleData, Oracle, IdAntiFreezeBase, IdAntiFreeze;
type
TForm1 = class(TForm)
OracleSession1: TOracleSession;
OracleDataSet1: TOracleDataSet;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DataSetProvider1: TDataSetProvider;
ClientDataSet1: TClientDataSet;
IdTCPClient1: TIdTCPClient;
IdTCPServer1: TIdTCPServer;
Button1: TButton;
Memo1: TMemo;
IdAntiFreeze1: TIdAntiFreeze;
procedure Button1Click(Sender: TObject);
procedure IdTCPServer1Connect(AThread: TIdPeerThread);
procedure IdTCPServer1Execute(AThread: TIdPeerThread);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure variantToStream(const v: oleVariant; stream: TStream);
var
p: pointer;
begin
stream.position := 0;
stream.size := varArrayHighBound(v, 1) - varArrayLowBound(v, 1) + 1;
p := varARrayLock(v);
stream.write(p^, stream.size);
varARrayUnlock(v);
stream.position := 0;
end;
procedure VarArrayToStream(const Data: OleVariant; Stream: TStream);
var
p: Pointer;
begin
p := VarArrayLock(Data);
try
Stream.Write(p^, VarArrayHighBound(Data, 1) + 1); //assuming low bound = 0
finally
VarArrayUnlock(Data);
end;
end;
function StreamToVarArray(Stream: TStream): OleVariant;
var
p: Pointer;
begin
Result := VarArrayCreate([0, Stream.Size - 1], varByte);
p := VarArrayLock(Result);
try
Stream.Position := 0; //start from beginning of stream
Stream.Read(p^, Stream.Size);
finally
VarArrayUnlock(Result);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ms: TMemoryStream;
begin
ms := TMemoryStream.Create;
if not IdTCPClient1.Connected then
IdTCPClient1.Connect;
if IdTCPClient1.Connected then
Memo1.Lines.Add('connected')
else
Memo1.Lines.Add('Not Connected');
IdTCPClient1.Write('open');
IdTCPClient1.ReadStream(ms, STrToINt(IdTCPClient1.ReadString(10)));
ClientDataSet1.Data := StreamToVarArray(ms);
// ClientDataSet1.LoadFromStream(ms);
ClientDataSet1.Active := True;
end;
procedure TForm1.IdTCPServer1Connect(AThread: TIdPeerThread);
begin
//
end;
procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
var
s: string;
ms: TMemoryStream;
begin
with AThread.Connection do
begin
s := ReadString(4);
if s = 'open' then
begin
ms := TMemoryStream.Create;
VarArrayToStream(DataSetProvider1.Data, ms);
s := IntToSTr(ms.Size);
while length(s) < 10 do
begin
s := '0' + s;
end;
Write(s);
ms.Seek(0, soFromBeginning);
WriteStream(ms);
end;
end;
end;
end.
2010. október 15., péntek
How to change the system colours
Problem/Question/Abstract:
How to change the system colours
Answer:
Solve 1:
procedure TMainForm.Button4Click(Sender: TObject);
var
nColorIndex: array[1..2] of integer;
nColorValue: array[1..2] of longint;
begin
nColorIndex[1] := COLOR_ACTIVECAPTION;
nColorIndex[2] := COLOR_BTNFACE;
nColorValue[1] := clBlue; {define the color you want}
nColorValue[2] := clRed; {in that case is the caption bar and button color}
SetSysColors(2, nColorIndex, nColorValue);
PostMessage(HWND_BROADCAST, WM_SYSCOLORCHANGE, 0, 0);
end;
You could have a look into the "Win32 API reference", under the "SetSysColors" section. There, if you directly go to "GetSysColors" you'll get alist of the places where you can change the colors. (e.g taskbar, borders, etc). In your case use COLOR_BACKGROUND and COLOR_DESKTOP.
Solve 2:
procedure TForm1.Button1Click(Sender: TObject);
const
ColCount = 2;
Elements: array[0..ColCount - 1] of Integer = (COLOR_WINDOW, COLOR_WINDOWTEXT);
Colors: array[0..ColCount - 1] of TColorRef = (clBlue, clYellow);
begin
if not SetSysColors(ColCount, Elements[0], Colors[0]) then
RaiseLastWin32Error;
end;
2010. október 14., csütörtök
How to sort a TStringList by numerical value using the Heapsort algorithm
Problem/Question/Abstract:
I cannot use the Sort method in TStringList as I would like to sort by Integer. My TStringList is filled with numbers such as:
20, 12, 1, 23, 54, 32
Of course, they're converted to string before being added to TStringList. What is a fast algorithm to achieve this sort? I normally have less than 50 items in my TStringList, if that is a factor.
Answer:
You'd end up doing a lot of conversions using StrToInt, which is wasteful, so I would recommend that you create a
type
PInteger = ^Integer type
store all of the StrToInt values in the TStringList.Objects array, and then when you use the sort, do your comparisons based on
PInteger(SL.Objects[Idx])^
The quicksort that TStringList uses (see CLASSES.PAS) uses a very simple partition function, which is completely unaware of the data it's sorting. It's using the midpoint index to begin to decide where to start partitioning, which is just as reliable as picking a random number when deciding how to sort. If, for example, you had a big list of items that was already sorted in the reverse direction, and you used this quicksort on it, and would call itself recursively once for every element in the list! Now, when you take into account that you're pushing a few items on the stack (the return address as well as the parameters as well as the registers you are saving) it might not take too long for your 16K of stack space to get eaten up (16,384 bytes divided by about maybe 32 bytes (and that's being pretty optimistic!) is about 2048 items before you run the risk of killing the stack!). The MaxListSize in CLASSES is 16380 (65520 div sizeof (Pointer)), so it's certainly possible to cause this problem.
Remember that TStringList.Sort is declared as virtual, so if you wanted to override it, you certainly could in a class derived from TStringList.
Also mind that the odds of anyone having to sort this much data (2000 items) seems pretty remote (correct me, anyone, if you've ever sorted more than 2000 strings in an application). The most reliable sort with the same running time as QuickSort is a HeapSort. They both run in O(N lg N) time, whereas sorts like the InsertionSort (which someone mentioned) and BubbleSort (which someone else mentioned) run in O(N^2) time, on the average.
The biggest differences between HeapSort and QuickSort, in terms of their run time and storage are:
HeapSort only calls itself recursively at most lg N times, where as QuickSort could call itself recursively N times (big difference, like 10 vs 1024, or 32 vs 2^32);
The worst case upper bound time on HeapSort is only O(N lg N), whereas in the worst case for QuickSort, the running time is O(N^2).
program H;
uses
WinCrt, SysUtils;
const
min = 10;
max = 13;
maxHeap = 1 shl max;
type
heap = array[1..maxHeap] of integer;
heapBase = ^heap;
var
currentSize, heapSize: integer;
A: heapBase;
procedure SwapInts(var a, b: integer);
var
t: integer;
begin
t := a;
a := b;
b := t
end;
procedure InitHeap(size: integer);
var
i: integer;
begin
heapSize := size;
currentSize := size;
Randomize;
for i := 1 to size do
A^[i] := Random(size) + 1;
end;
procedure Heapify(i: integer);
var
left, right, largest: integer;
begin
largest := i;
left := 2 * i;
right := left + 1;
if left <= heapSize then
if A^[left] > A^[i] then
largest := left;
if right <= heapSize then
if A^[right] > A^[largest] then
largest := right;
if largest <> i then
begin
SwapInts(A^[largest], A^[i]);
Heapify(largest)
end;
end;
procedure BuildHeap;
var
i: integer;
begin
for i := heapSize div 2 downto 1 do
Heapify(i)
end;
procedure HeapSort;
var
i: integer;
begin
BuildHeap;
for i := currentSize downto 2 do
begin
SwapInts(A^[i], A^[1]);
dec(heapSize);
Heapify(1)
end;
end;
type
TAvgTimes = array[min..max] of TDateTime;
var
sTime, eTime, tTime: TDateTime;
i, idx, size: integer;
avgTimes: TAvgTimes;
begin
tTime := 0;
i := min;
size := 1 shl min;
new(A);
while i <= max do
begin
for idx := 1 to 10 do
begin
InitHeap(size);
sTime := Time;
HeapSort;
eTime := Time;
tTime := tTime + (eTime - sTime)
end;
avgTimes[i] := tTime / 10.0;
inc(i);
size := size shl 1;
end;
end.
2010. október 13., szerda
Get "executable" file name also from a DLL
Problem/Question/Abstract:
If you still use ParamStr(0), or Application.ExeName for getting your executable path and file name, you could have problems developing DLLs. In fact if your DLL enquires ParamStr(0) it gets the path and file name of the executable which loaded your DLL.
Answer:
Using Application.ExeName is the same as using ParamStr(0), but there's a way to fix this and have the correct file name in every case.
Just use this:
function GetRealExeName: string;
var
ExeName: array[0..MAX_PATH] of char;
begin
fillchar(ExeName, SizeOf(ExeName), #0);
GetModuleFileName(HInstance, ExeName, MAX_PATH);
Result := ExeName;
end;
2010. október 12., kedd
Implement fuzzy search
Problem/Question/Abstract:
How to implement fuzzy search
Answer:
Solve 1:
This DLL calculates the Levenshtein Distance between two strings. Please note that ShareMem must be the first unit in the Uses clause of the Interface section of your unit, if your DLL exports procedures or functions, which pass string parameters or function results. ShareMem is the interface to delphimm.dll, which you have to distribute together with your own DLL. To avoid using delphimm.dll, pass string parameters by using PChar or ShortString parameters.
library Levensh;
uses
ShareMem, SysUtils;
var
FiR0: integer;
FiP0: integer;
FiQ0: integer;
function Min(X, Y, Z: Integer): Integer;
begin
if (X < Y) then
Result := X
else
Result := Y;
if (Result > Z) then
Result := Z;
end;
procedure LevenshteinPQR(p, q, r: integer);
begin
FiP0 := p;
FiQ0 := q;
FiR0 := r;
end;
function LevenshteinDistance(const sString, sPattern: string): Integer;
const
MAX_SIZE = 50;
var
aiDistance: array[0..MAX_SIZE, 0..MAX_SIZE] of Integer;
i, j, iStringLength, iPatternLength, iMaxI, iMaxJ: Integer;
chChar: Char;
iP, iQ, iR, iPP: Integer;
begin
iStringLength := length(sString);
if (iStringLength > MAX_SIZE) then
iMaxI := MAX_SIZE
else
iMaxI := iStringLength;
iPatternLength := length(sPattern);
if (iPatternLength > MAX_SIZE) then
iMaxJ := MAX_SIZE
else
iMaxJ := iPatternLength;
aiDistance[0, 0] := 0;
for i := 1 to iMaxI do
aiDistance[i, 0] := aiDistance[i - 1, 0] + FiR0;
for j := 1 to iMaxJ do
begin
chChar := sPattern[j];
if ((chChar = '*') or (chChar = '?')) then
iP := 0
else
iP := FiP0;
if (chChar = '*') then
iQ := 0
else
iQ := FiQ0;
if (chChar = '*') then
iR := 0
else
iR := FiR0;
aiDistance[0, j] := aiDistance[0, j - 1] + iQ;
for i := 1 to iMaxI do
begin
if (sString[i] = sPattern[j]) then
iPP := 0
else
iPP := iP;
{aiDistance[i, j] := Minimum of 3 values}
aiDistance[i, j] := Min(aiDistance[i - 1, j - 1] + iPP,
aiDistance[i, j - 1] + iQ,
aiDistance[i - 1, j] + iR);
end;
end;
Result := aiDistance[iMaxI, iMaxJ];
end;
exports
LevenshteinDistance Index 1,
LevenshteinPQR Index 2;
begin
FiR0 := 1;
FiP0 := 1;
FiQ0 := 1;
end.
Solve 2:
This is an old Pascal code snippet, which is based on a C project published in the C't magazine somewhen back in the 1990's. Can't remember where I found it on the WWW. Please note that the code below accesses a simple *.txt file to search in.
program FuzzySearch;
{Translation from C to Pascal by Karsten Paulini and Simon Reinhardt}
const
MaxParLen = 255;
var
InFile: Text;
Filename: string;
InputStr: string;
SearchStr: string;
Treshold: Integer;
function PrepareTheString(OriginStr: string; var ConvStr: string): Integer;
var
i: Integer;
begin
ConvStr := OriginStr;
for i := 1 to Length(OriginStr) do
begin
ConvStr[i] := UpCase(ConvStr[i]);
if ConvStr[i] < '0' then
ConvStr[i] := ' '
else
case ConvStr[i] of
Chr(196): ConvStr[i] := Chr(228);
Chr(214): ConvStr[i] := Chr(246);
Chr(220): ConvStr[i] := Chr(252);
Chr(142): ConvStr[i] := Chr(132);
Chr(153): ConvStr[i] := Chr(148);
Chr(154): ConvStr[i] := Chr(129);
':': ConvStr[i] := ' ';
';': ConvStr[i] := ' ';
'<': ConvStr[i] := ' ';
'>': ConvStr[i] := ' ';
'=': ConvStr[i] := ' ';
'?': ConvStr[i] := ' ';
'[': ConvStr[i] := ' ';
']': ConvStr[i] := ' ';
end;
end;
PrepareTheString := i;
end;
function NGramMatch(TextPara, SearchStr: string; SearchStrLen, NGramLen: Integer;
var MaxMatch: Integer): Integer;
var
NGram: string[8];
NGramCount: Integer;
i, Count: Integer;
begin
NGramCount := SearchStrLen - NGramLen + 1;
Count := 0;
MaxMatch := 0;
for i := 1 to NGramCount do
begin
NGram := Copy(SearchStr, i, NGramLen);
if (NGram[NGramLen - 1] = ' ') and (NGram[1] < > ' ') then
Inc(i, NGramLen - 3) {will be increased in the loop}
else
begin
Inc(MaxMatch, NGramLen);
if Pos(NGram, TextPara) > 0 then
Inc(Count);
end;
end;
NGramMatch := Count * NGramLen;
end;
procedure FuzzyMatching(SearchStr: string; Treshold: Integer; var InFile: Text);
var
TextPara: string;
TextBuffer: string;
TextLen: Integer;
SearchStrLen: Integer;
NGram1Len: Integer;
NGram2Len: Integer;
MatchCount1: Integer;
MatchCount2: Integer;
MaxMatch1: Integer;
MaxMatch2: Integer;
Similarity: Real;
BestSim: Real;
begin
BestSim := 0.0;
SearchStrLen := PrepareTheString(SearchStr, SearchStr);
NGram1Len := 3;
if SearchStrLen < 7 then
NGram2Len := 2
else
NGram2Len := 5;
while not Eof(InFile) do
begin
Readln(InFile, TextBuffer);
TextLen := PrepareTheString(TextBuffer, TextPara) + 1;
TextPara := Concat(' ', TextPara);
if TextLen < MaxParLen - 2 then
begin
MatchCount1 := NGramMatch(TextPara, SearchStr, SearchStrLen, NGram1Len, MaxMatch1);
MatchCount2 := NGramMatch(TextPara, SearchStr, SearchStrLen, NGram2Len, MaxMatch2);
Similarity := 100.0 * (MatchCount1 + MatchCount2) / (MaxMatch1 + MaxMatch2);
if Similarity > BestSim then
BestSim := Similarity;
if Similarity >= Treshold then
begin
Writeln;
Writeln('[', Similarity, '] ', TextBuffer);
end;
end;
else
Writeln('Paragraph too long');
end;
if BestSim < Treshold then
Writeln('No match; Best Match was ', BestSim);
end;
begin
Writeln;
Writeln('+------------------------------------------+');
Writeln('| Fuzzy Search in Information Retrieval |');
Writeln('| (C) 1997 Reinhard Rapp |');
Writeln('+------------------------------------------+');
Writeln;
Write('Name of file to search in: ');
Readln(Filename);
Write('Search string: ');
Readln(InputStr);
SearchStr := Concat(' ', InputStr, ' ');
Write('Minimum hit quality in % : ');
Readln(Treshold);
if (Treshold > 0) and (Treshold <= 100) and (SearchStr < > '') and (Filename < > '') then
begin
Assign(InFile, Filename);
Reset(InFile);
FuzzyMatching(SearchStr, Treshold, InFile);
Close(InFile);
end;
Writeln;
Writeln('Bye!');
end.
Solve 3:
unit FuzzyMatch;
{This unit provides a basic 'fuzzy match' index on how alike two strings are
The result is of type 'single': near 0 - poor match
near 1 - close match
The intention is that HowAlike(s1,s2)=HowAlike(s2,s1)
The Function is not case sensitive}
interface
uses sysutils;
function HowAlike(s1, s2: string): single;
implementation
function instr(start: integer; ToSearch, ToFind: string): integer;
begin
//This is a quick implementation of the VB InStr, since Pos just doesn't do what is needed!!
//NB - case sensitive!!
if start > 1 then
Delete(ToSearch, 1, start - 1);
result := pos(ToFind, ToSearch);
if (result > 0) and (start > 1) then
inc(result, start);
end;
function HowAlike(s1, s2: string): single;
var
l1, l2, pass, position, size, foundpos, maxscore: integer;
score, scored, string1pos, string2pos, bestmatchpos: single;
swapstring, searchblock: string;
begin
s1 := Uppercase(trim(s1));
s2 := Uppercase(trim(s2));
score := 0;
maxscore := 0;
scored := 0;
//deal with zero length strings...
if (s1 = '') and (s2 = '') then
begin
result := 1;
exit;
end
else if (s1 = '') or (s2 = '') then
begin
result := 0;
exit;
end;
//why perform any mathematics is the result is clear?
if s1 = s2 then
begin
result := 1;
exit;
end;
//make two passes,
// with s1 and s2 each way round to ensure
// consistent results
for pass := 1 to 2 do
begin
l1 := length(s1);
l2 := length(s2);
for size := l1 downto 1 do
begin
for position := 1 to (l1 - size + 1) do
begin
//try to find implied block in the other string
//Big blocks score much better than small blocks
searchblock := copy(s1, position, size);
foundpos := pos(searchblock, s2);
if size = l1 then
string1pos := 0.5
else
string1pos := (position - 1) / (l1 - size);
if foundpos > 0 then
begin
//the string is in somewhere in there
// - find the 'closest' one.
bestmatchpos := -100; //won't find anything that far away!
repeat
if size = l2 then
string2pos := 0.5
else
string2pos := (foundpos - 1) / (l2 - size);
//If this closer than the previous best?
if abs(string2pos - string1pos) < abs(bestmatchpos - string1pos) then
bestmatchpos := string2pos;
foundpos := instr(foundpos + 1, s2, searchblock);
until foundpos = 0; //loop while foundpos>0..
//The closest position is now known: Score it!
//Score as follows: (1-distance of best match)
score := score + (1 - abs(string1pos - bestmatchpos));
end;
//Keep track if the maximum possible score
//BE CAREFUL IF CHANGING THIS FUNCTION!!!
//maxscore:=maxscore+1;
inc(maxscore);
end; //for position..
end; //for size..
if pass = 1 then
begin
//swap the strings around
swapstring := s1;
s1 := s2;
s2 := swapstring;
end;
//Each pass is weighted equally
scored := scored + (0.5 * (score / maxscore));
score := 0;
maxscore := 0;
end; //for pass..
//HowAlike=score/maxscore
result := scored;
end;
Solve 4:
A Delphi implementation of the Levenshtein Distance Algorithm
unit Levenshtein;
{Objeto que calcula la distancia de Levenshtein entre 2 cadenas.
Alvaro Jeria Madariaga. 04/10/2002
barbaro@hotpop.com}
interface
uses
sysutils, Math;
type
Tdistance = class(TObject)
private
function minimum(a, b, c: Integer): Integer;
public
function LD(s, t: string): Integer;
end;
implementation
function Tdistance.minimum(a, b, c: Integer): Integer;
var
mi: Integer;
begin
mi := a;
if (b < mi) then
mi := b;
if (c < mi) then
mi := c;
Result := mi;
end;
function Tdistance.LD(s, t: string): Integer;
var
d: array of array of Integer;
n, m, i, j, costo: Integer;
s_i, t_j: char;
begin
n := Length(s);
m := Length(t);
if (n = 0) then
begin
Result := m;
Exit;
end;
if m = 0 then
begin
Result := n;
Exit;
end;
setlength(d, n + 1, m + 1);
for i := 0 to n do
d[i, 0] := i;
for j := 0 to m do
d[0, j] := j;
for i := 1 to n do
begin
s_i := s[i];
for j := 1 to m do
begin
t_j := t[j];
if s_i = t_j then
costo := 0
else
costo := 1;
d[i, j] := Minimum(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + costo);
end;
end;
Result := d[n, m];
end;
end.
2010. október 11., hétfő
MessageDlg hidden by main form (XP only)
Problem/Question/Abstract:
When calling MessageDlg (and also on certain TForm forms on calling showModal) the dialog sometimes pops up under the main form, on Windows XP systems. This does not appear to affect other operating systems.
Answer:
Setting HKEY_CURRENT_USER\Control Panel\Desktop\ForegroundLockTimeout=0 seems to fix the problem... but WHY? According to MSDN (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/WinUI/WindowsUserInterface/Windowing/Windows/WindowReference/WindowFunctions/SetForegroundWindow.asp) the new window should be able to go to the foreground if the process is the foreground process. The process was started by the foreground process. The process received the last input event. There is no foreground process. The foreground process is being debugged. The foreground is not locked (see LockSetForegroundWindow). The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo). Windows 2000/XP: No menus are active.
Ok, well having checked the last point there, calling Application.ProcessMessages before popping up the dialog fixes the problem!
---
This solution taken from the DOMAJ forum: http://www.domaj.com/forum/viewthread.php?tid=348
When calling MessageDlg (and also on certain TForm forms on calling showModal) the dialog sometimes pops up under the main form, on Windows XP systems. This does not appear to affect other operating systems.
Answer:
Setting HKEY_CURRENT_USER\Control Panel\Desktop\ForegroundLockTimeout=0 seems to fix the problem... but WHY? According to MSDN (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/WinUI/WindowsUserInterface/Windowing/Windows/WindowReference/WindowFunctions/SetForegroundWindow.asp) the new window should be able to go to the foreground if the process is the foreground process. The process was started by the foreground process. The process received the last input event. There is no foreground process. The foreground process is being debugged. The foreground is not locked (see LockSetForegroundWindow). The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo). Windows 2000/XP: No menus are active.
Ok, well having checked the last point there, calling Application.ProcessMessages before popping up the dialog fixes the problem!
---
This solution taken from the DOMAJ forum: http://www.domaj.com/forum/viewthread.php?tid=348
2010. október 10., vasárnap
Case statement that *accepts* string values
Problem/Question/Abstract:
You've probably tried providing a Case statement with string type selector expression, to find out that it only takes ordinal types (which string is not).
The following function enables you to use the Case statement with string type variables:
Answer:
function StringToCaseSelect
(Selector : string;
CaseList: array of string): Integer;
var cnt: integer;
begin
Result:=-1;
for cnt:=0 to Length(CaseList)-1 do
begin
if CompareText(Selector, CaseList[cnt]) = 0 then
begin
Result:=cnt;
Break;
end;
end;
end;
{
Usage:
case StringToCaseSelect('Delphi',
['About','Borland','Delphi']) of
0:ShowMessage('You''ve picked About') ;
1:ShowMessage('You''ve picked Borland') ;
2:ShowMessage('You''ve picked Delphi') ;
end;
}
You've probably tried providing a Case statement with string type selector expression, to find out that it only takes ordinal types (which string is not).
The following function enables you to use the Case statement with string type variables:
Answer:
function StringToCaseSelect
(Selector : string;
CaseList: array of string): Integer;
var cnt: integer;
begin
Result:=-1;
for cnt:=0 to Length(CaseList)-1 do
begin
if CompareText(Selector, CaseList[cnt]) = 0 then
begin
Result:=cnt;
Break;
end;
end;
end;
{
Usage:
case StringToCaseSelect('Delphi',
['About','Borland','Delphi']) of
0:ShowMessage('You''ve picked About') ;
1:ShowMessage('You''ve picked Borland') ;
2:ShowMessage('You''ve picked Delphi') ;
end;
}
2010. október 9., szombat
Create data-aware components
Problem/Question/Abstract:
How to create data-aware components
Answer:
This document describes minimal steps necessary to create a data-aware browsing component that displays data for a single field. The example component is a panel with DataSource and DataField properties similar to the TDBText component. See the Component Writer's Guide "Making a Control Data-Aware" for further examples.
Basic steps to create a data-browsing component
Create or derive a component that allows the display, but not the entry of data. For instance, you could use a TMemo with ReadOnly set to true. In the example outlined in this document, we'll use a TCustomPanel. The TCustomPanel will allow display, but not data entry.
Add a data-link object to your component. This object manages communication between the component and the database table.
Add DataField and DataSource properties to the component.
Add methods to get and set the DataField and DataSource.
Add a DataChange method the component to handle the data-link object's OnDataChange event.
Override the component constructor to create the datalink and hook up the DataChange method.
Override the component destructor to cleanup the datalink.
Creating the TDBPanel
Create or derive a component that allows the display, but not the entry of data. We'll be using a TCustomPanel as a starting point for this example.
Choose the appropriate menu option to create a new component (this will vary between editions of Delphi), and specify TDBPanel as the class name, and TCustomPanel as the Ancestor type. You may specify any palette page.
Add DB and DBTables to your Uses clause.
Add a data-link object to the components private section. This example will display data for a single field, so we will use a TFieldDataLink to provide the connection between our new component and a DataSource. Name the new data-link object FDataLink. Example:
private
FDataLink: TFieldDataLink;
Add DataField and DataSource properties to the component. We will add supporting code for the get and set methods in following steps. Note: Our new component will have DataField and DataSource properties and FDataLink will also have its own DataField and Datasource properties. Example:
published
property DataField: string read GetDataField write SetDataField;
property DataSource: TDataSource read GetDataSource write SetDataSource;
Add private methods to get and set the DataField and DataSource property values to and from the DataField and DataSource for FDataLink. Example:
private
FDataLink: TFieldDataLink;
function GetDataField: string;
function GetDataSource: TDataSource;
procedure SetDataField(const Value: string);
procedure SetDataSource(Value: TDataSource);
implementation
function TDBPanel.GetDataField: string;
begin
Result := FDataLink.FieldName;
end;
function TDBPanel.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TDBPanel.SetDataField(const Value: string);
begin
FDataLink.FieldName := Value;
end;
procedure TDBPanel.SetDataSource(Value: TDataSource);
begin
FDataLink.DataSource := Value;
end;
Add a private DataChange method to be assigned to the datalink's OnDataChange event. In the DataChange method add code to display actual database field data provided by the data-link object. In this example, we assign FDataLink's field value to the panel's caption. Example:
private
procedure DataChange(Sender: TObject);
implementation
procedure TDBPanel.DataChange(Sender: TObject);
begin
if FDataLink.Field = nil then
Caption := '';
else
Caption := FDataLink.Field.AsString;
end;
Override the component constructor Create method. In the implementation of Create, create the FDataLink object, and assign the private DataChange method to FDataLink's OnDataChange event. Example:
public
constructor Create(AOwner: TComponent); override;
implementation
constructor TMyDBPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FDataLink := TFieldDataLink.Create;
FDataLink.OnDataChange := DataChange;
end;
Override the component destructor Destroy method. In the implementation of Destroy, set OnDataChange to nil (avoids a GPF), and free FDatalink. Example:
public
destructor Destroy; override;
implementation
destructor TDBPanel.Destroy;
begin
FDataLink.OnDataChange := nil;
FDataLink.Free;
inherited Destroy;
end;
Save the unit and install the component (see the Users Guide, and the Component Writers Guide for more on saving units and installing components).
To test the functionality of the component, add a TTable, TDatasource, TDBNavigator and TDBPanel to a form. Set the TTable DatabaseName and Tablename to 'DBDemos' and 'BioLife', and the Active property to True. Set the TDatasource Dataset property to Table1. Set the TDBNavigator and TDBPanel DataSource property to Datasource1. The TDBpanel DataField name should be set as 'Common_Name'. Run the application and use the navigator to move between records to demonstrate the TDBPanel's ability to detect the change in data and display the appropriate field value.
Full source listing:
unit Mydbp;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics,
Controls, Forms, Dialogs, ExtCtrls, DB, DBTables;
type
TDBPanel = class(TCustomPanel)
private
FDataLink: TFieldDataLink;
function GetDataField: string;
function GetDataSource: TDataSource;
procedure SetDataField(const Value: string);
procedure SetDataSource(Value: TDataSource);
procedure DataChange(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property DataField: string read GetDataField write SetDataField;
property DataSource: TdataSource read GetDataSource write SetDataSource;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TDBPanel]);
end;
function TDBPanel.GetDataField: string;
begin
Result := FDataLink.FieldName;
end;
function TDBPanel.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TDBPanel.SetDataField(const Value: string);
begin
FDataLink.FieldName := Value;
end;
procedure TDBPanel.SetDataSource(Value: TDataSource);
begin
FDataLink.DataSource := Value;
end;
procedure TDBPanel.DataChange(Sender: TObject);
begin
if FDataLink.Field = nil then
Caption := ''
else
Caption := FDataLink.Field.AsString;
end;
constructor TDBPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FDataLink := TFieldDataLink.Create;
FDataLink.OnDataChange := DataChange;
end;
destructor TDBPanel.Destroy;
begin
FDataLink.Free;
FDataLink.OnDataChange := nil;
inherited Destroy;
end;
end.
2010. október 8., péntek
Beep/Sound in Delphi
Problem/Question/Abstract:
Beep/Sound in Delphi
Answer:
The following assembler Routines implement sound output via port access and work therefore only with Win3.x and Win95/98. Simply call Sound(hz) with hz as frequency in Hz, and stop the sound output with NoSound().
If your application will run under Windows NT, you may use the operating system routine:
Windows.Beep(Frequency, Duration);
function InPort(PortAddr: word): byte; assembler; stdcall;
asm
mov dx,PortAddr
in al,dx
end;
procedure OutPort(PortAddr: word; Databyte: byte); assembler; stdcall;
asm
mov al,Databyte
mov dx,PortAddr
out dx,al
end;
procedure Sound(Hz: Word);
var
TmpW: Word;
begin
OutPort($43, 182);
TmpW := InPort($61);
OutPort($61, TmpW or 3);
OutPort($42, lo(1193180 div hz));
OutPort($42, hi(1193180 div hz));
end;
procedure NoSound;
var
TmpW: Word;
begin
OutPort($43, 182);
TmpW := InPort($61);
OutPort($61, TmpW and 3);
end;
2010. október 7., csütörtök
TIniFile ini files are limited to 64KB - how to go beyond 64KB
Problem/Question/Abstract:
TIniFile ini files are limited to 64KB - how to go beyond 64KB
Answer:
The TIniFile class uses the Windows API which imposes a limit of 64KB on INI files. If you need to store more than 64KB of data, you may want to use TMemIniFile instead. TMemIniFile does not have a limit of 64KB.
Important:
Remember to call the UpdateFile() method when you need the data to be written to disk: it does not do that automatically.
2010. október 6., szerda
How to check if a date exists
Problem/Question/Abstract:
Is there a possibility to check if a date exists (e.g. 35.3.2001)?
Answer:
function DateExists(Date: string; Separator: char): Boolean;
var
OldDateSeparator: Char;
begin
Result := True;
OldDateSeparator := DateSeparator;
DateSeparator := Separator;
try
try
StrToDate(Date);
except
Result := False;
end;
finally
DateSeparator := OldDateSeparator;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
if DateExists('35.3.2001', '.') then
begin
{your code}
end;
end;
2010. október 5., kedd
BDE error codes by code
Problem/Question/Abstract:
Show/generate the BDE errors list.
Answer:
The list of errors of the BDE, we can obtain it investigating a little in the file bde.int
There we will see that the error codes are composed of a value ' base' and of an offset.
Here you have an invention to generate your listing of errors of the BDE:
Add 'dbiprocs' in the uses of your form
Put a TRichEdit (RE1)
And put this in the OnClick of a TButton:
procedure TForm1.Button1Click(Sender: TObject);
const
Bases:array [1..24] of integer=(
0,$2100,$2200,$2300,$2400,$2500,$2600,$2700,$2800,
$2900,$2A00,$2B00,$2C00,$2D00,$2E00,$2F00,$3000,
$3100,$3200,$3300,$3400,$3500,$3E00,$3F00);
var
ErrorCod:integer;
ErrorTexto:array [0..DBIMAXMSGLEN+1] of char;
i,n:integer;
begin
for i:=1 to 24 do
for n:=0 to 255 do
begin
ErrorCod:=Bases[i]+n;
DbiGetErrorString(ErrorCod,ErrorTexto);
if ErrorTexto<>'' then
Re1.Lines.Add('$'+IntToHex(ErrorCod,4)+' ('+
IntToStr(ErrorCod)+') = '+ErrorTexto);
Application.ProcessMessages;
end;
end;
2010. október 4., hétfő
How to define the client area of a window
Problem/Question/Abstract:
Is it possible to define (set the size and position) of a window's client area (without resizing the window itself)? What I want to do is increase the non-client area to get more space to paint my own custom borders around a window. I want this to be reflected to the client area so that my border is protected from anything that goes on in the client area (painting, scrolling etc.).
Answer:
You have to handle the WM_NCCALCSIZE message on your form. See win32.hlp for details. The following example handler for a TListBox descendent excludes some space for a header bar from the listboxes client area:
procedure THeaderListbox.wmnccalcsize(var msg: TWMNCCALCSIZE);
begin
inherited;
if msg.CalcValidRects then
with msg.CalcSize_Params^.rgrc[0] do
top := top + Itemheight + 4;
end;
I hope you know how to define a message handler in the class declaration.
2010. október 3., vasárnap
An approach to implement alternative C/S-like database solutions without having a C/S engine
Problem/Question/Abstract:
An approach to implement alternative C/S-like database solutions without having a C/S engine
Answer:
I just came from a successful demo to our client. I used ASTA as my messaging middleware together with DBISAM. I did not use the ASTADBISAM server, just the plain ASTAServerSocket.
Here's what I did: The main concept is that only the ASTAServer socket writes to the database tables. All the data that needed to be written come from the ASTA clients and received by ASTAServer socket which writes these information into the database. The Database Tables on the Server machine are shared across all clients as READ-ONLY. All my ASTAClient applications synchronizes the lookup tables through a shared file access and not trhough ASTA. These took advantage of the LAN situation. We tested with 40 clients connected (ASTA client and shared READ-ONLY), the database flies! I avoided using ClientDataSets and AstaClientDataSets. Then we began pulling the plug on some of the Client Machines, and never was there any corruption on the main data.
I am excited because all of the client programs behave as if they are all acting like a single-user local connections! With 40 online connections, it takes under a second to retrieve and post hundreds of detail records from the server, and 40 users doing all of it at the same time!
Now I am at a point of optimizing between a LAN (shared read-only database, TCP/IP write by AstaServerSocket) and a pure TCP/IP connectivity. I am trying to create a client program that will take advantage of a LAN connection whenever available (right now, this is set manually during the initial setup of the client program).
So are you saying that you are reading the data from the clients using standard DBISAM table and/or query components, then writing changes back to the database through ASTA? Can you give us a more concrete example of how you set this up?
The clients are reading the data read-only from a shared folder. Then they are modified on the temporary tables on the client side, then only the modified portions (deltas) are sent to the AstaServer through a coded paramlist. The Astaserver receives the coded paramlist and depending upon the code, parse the data into destination tables for updates or writes, inserts, appends, deletes. One thing nice, is I can code everything on the Server side through tables and filters, wrap it around transactions, and never have to worry about corruption anymore. It is the server which does the actual writing to the shared data. This way, no client would be able to delete the data by accident on the shared folder. Only the server has the wread/write access to the data. Only occassionally I have to send data back to the clients, through coded paramlist via the socket components, that is only when they are registered to be on a modem line. Otherwise, if they are on a LAN, it is always faster for the clients to read the shared tables from a READ-ONLY folder, because the client's computer can do their own individual buffering of the database tables at a far greater capacity and efficiency than routing everything through the socket layer. With respect to security, the password table on the sahred folder is encrypted with DBISAM's own encryption, but not only that, the data contents of fields themselves are individually encrypted with my own encryption, so no one is able to get passwords and user id, they may be able to get the password's table and read the data structure but not the contents which would remain garbled unless they know how to decrypt them using my own algo. This is very good security for me.
My setup was creating the data on the Win2000 server, then it is shared as read-only. Since the AstaServer resides on the Win2000, it is the only program that has direct read/ write privileges to the data unless you set it otherwise.
Here are samples of my client-side codes sent to the server from the client's temporary tables. The whole process really works at lightning speed.
procedure TDML.PostMTO(const aMTONO: integer; aNewStatus, aHeaders, aNotes: string);
var
lnstr: string;
MTOParams, RetParams: TAstaParamList;
begin
Screen.Cursor := crHourGlass;
RetParams := TAstaParamList.Create;
MTOParams := TAstaParamList.Create;
try
LoadTmpList(pvMTmpList, pvWTmpList);
MTOParams.Add;
MTOParams[0].Name := UserLoginID;
MTOParams[0].AsInteger := aMTONO;
MTOParams.Add;
MTOParams[1].Name := aNewStatus;
MTOParams[1].AsString := aHeaders;
MTOParams.FastAdd(aNotes);
MTOParams.FastAdd(pvMTmpList.Text);
MTOParams.FastAdd(pvWTmpList.Text);
RetParams := AstaClientSocket1.SendGetCodedParamList(2100, MTOParams);
lnstr := RetParams[0].AsString;
ShowMessage('MTO successfully posted at server time: ' + lnstr);
finally
Screen.Cursor := crDefault;
MTOParams.Free;
RetParams.Free;
pvMTmpList.Clear;
pvWTmpList.Clear;
end;
end;
And here is how a server could receive them and call the server's datamodule to write:
procedure TIsoFabForm.AstaServerSocket1CodedParamList(Sender: TObject;
ClientSocket: TCustomWinSocket; MsgID: Integer; Params: TAstaParamList);
var
i: integer;
TmpStr, TmpStr2, ErrMsg, aUserID: string;
MList, WList: TStringList;
begin
case MsgID of
{...}
2100: {Post MTO}
begin
aUserID := Params[0].Name;
i := Params[0].AsInteger; {MTONo}
MList := TStringList.Create;
WList := TStringList.Create;
try
MList.Text := Params[3].Text;
WList.Text := Params[4].Text;
DMServer.PostMTO(i, aUserID, Params[1].Name, Params[1].AsString,
Params[2].AsString,
MList, WList, True);
Params.Clear;
Params.FastAdd(Now);
AstaServerSocket1.SendCodedParamList(ClientSocket, MsgID, Params);
finally
MList.Free;
WList.Free;
end;
end;
{....}
procedure TDMServer.PostMTO(const cMTONO: integer; aUserID, aNewStatus, aHeader,
aNotes: string; var MList, WList: TStringList; BalanceStock: boolean);
procedure PostItHere(const aTbl: TDBISAMTable; var aList: TStringList);
var
i, deltarecs: integer;
lnstr: string;
begin
aTbl.IndexName := 'MTONO';
aTbl.SetRange([cMTONo], [cMTONO]);
DeltaRecs := aList.Count - aTbl.RecordCount;
if DeltaRecs > 0 then
begin
for i := 1 to DeltaRecs do
begin
aTbl.Append;
aTbl.FieldByName('MTONO').AsInteger := cMTONO;
aTbl.Post;
end;
end;
if DeltaRecs < 0 then
begin
aTbl.First;
for i := 1 to -DeltaRecs do
aTbl.delete;
end;
aTbl.First;
for i := 0 to aList.Count - 1 do
begin
lnstr := aList[i];
aTbl.Edit;
aTbl.FieldByName('ItemNo').AsString := GetLeftWord(lnstr, #9);
aTbl.FieldByName('QCode').AsString := GetLeftWord(lnstr, #9);
aTbl.FieldByName('QtyNeed').AsString := GetLeftWord(lnstr, #9);
aTbl.FieldByName('QtyRel').AsString := GetLeftWord(lnstr, #9);
aTbl.FieldByName('QtyScraps').AsString := GetLeftWord(lnstr, #9);
aTbl.FieldByName('UnitCostRel').AsString := GetLeftWord(lnstr, #9);
aTbl.FieldByName('TagNo').AsString := GetLeftWord(lnstr, #9);
aTbl.Post;
aTbl.Next;
end;
aList.Clear;
end;
begin
if cMTONO <= 0 then
exit;
if not DB1.InTransaction then
DB1.StartTransaction;
MTOMain.IndexName := 'MTONO';
if not MTOMain.FindKey([cMTONO]) then
begin
MTOMain.Append;
MTOMain.FieldByName('MTONo').AsInteger := cMTONO;
end
else
begin
MTOMain.Edit;
end;
GetLeftWord(aHeader, #9); {discard first column which is MTONO}
MTOMain.FieldByName('Status').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('Project').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('Customer').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('ToolOrLateral').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('JobNo').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('SubJob').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DwgNo').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('SpoolNo').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('System').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('MaterialCode').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('LaborCode').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DateNeeded').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DateBuilt').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DateShipped').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DateDrawn').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DateRevised').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('DateRecvd').AsString := GetLeftWord(aHeader, #9);
MTOMain.FieldByName('SubmittedBy').AsString := aUserID;
MTOMain.FieldByName('Notes').AsString := aNotes;
MTOMain.Post;
if (BalanceStock) and (MTOMain.FieldByName('Status').AsString <> 'DS') then
begin
RecomputeMTO(cMTONO, False); {subtract existing MTO Items}
end;
PostItHere(MTOItems, MList);
PostItHere(WeldItems, WList);
if BalanceStock then
begin
RecomputeMTO(cMTONO, True); {add to stock New MTO Items}
end;
if DB1.InTransaction then
DB1.Commit;
end;
And of course, here is my versatile GetLeftWord function that can successively parse a given string into individual datafields or values:
function GetLeftWord(var ASentence: string; ADelimiter: char): string;
var
i: integer;
begin
Result := '';
i := Pos(ADelimiter, ASentence);
if i = 0 then
begin
Result := Trim(ASentence);
ASentence := '';
exit;
end;
if i = 1 then
Result := ''
else
Result := trim(Copy(ASentence, 1, i - 1));
Delete(ASentence, 1, i);
end;
I also made intensive use of routines like this to update any table, just pass it a series of strings:
procedure TDMServer.UpdateTable(var aTbl: TDBISAMTable; anIndexField, aFieldStr:
string);
var
anIndexValue, fldname, fldvalue: string;
begin
aTbl.IndexName := anIndexField;
anIndexValue := GetLeftWord(aFieldStr, #9);
if aTbl.FindKey([anIndexValue]) then
begin
aTbl.Edit;
end
else
begin
aTbl.Append;
aTbl.FieldByName(anIndexField).AsString := anIndexValue;
end;
while aFieldStr < > '' do
begin
fldname := GetLeftWord(aFieldStr, #9);
fldvalue := GetLeftWord(aFieldStr, #9);
if fldname = 'CDT' then
continue;
try
aTbl.FieldByName(fldname).AsString := fldvalue;
except
end;
end;
aTbl.FieldByName('CDT').AsDateTime := Now;
aTbl.Post;
aTbl.FlushBuffers;
end;
That's a very interesting approach and I can see how it could speed things up yet still give you the data integrity you look for with a client server approach. I can also see where it could simplify some of the typical c/s user interface issues as well. For instance you could let a client open an entire table, then view and scroll through the data in a grid component without having to send all of that data through the pipeline. Obviously you wouldn't want your remote clients to do that, but such screens could easily be limited to only the people connected via LAN. It would also allow multiple large queries to run simultaneously (such as for reports) without slowing up everything else. If the main objective is server side control of data (such as enforcement of business rules) and elimination of corruption then this technique should work very well.
2010. október 2., szombat
How to implement TCollection.SaveToStream
Problem/Question/Abstract:
I need to implement a streaming capability for a TCollection class object. Is there anyone who knows how to do it?
Answer:
I do it via the following two utility procedures:
procedure ReadCollection(s: TStream; c: TCollection);
var
Reader: TReader;
begin
Reader := TReader.Create(s, 1024);
try
Reader.ReadValue; {collection marker}
Reader.ReadCollection(c);
finally
Reader.Free;
end;
end;
procedure WriteCollection(s: TStream; c: TCollection);
var
Writer: TWriter;
begin
Writer := TWriter.Create(s, 1024);
try
Writer.WriteCollection(c);
finally
Writer.Free;
end;
end;
Both procedures assume that the stream has been created and positioned correctly.
2010. október 1., péntek
Ms Access LastinsertID
Problem/Question/Abstract:
Ever wondered how to retrieve the last insert id in MsAccess, of the autoincrement field from a table.
Answer:
We have a table in MsAccess like :
Test, Fields (id=autoinc, name=text);
First we have to have a function like the one below :
function GetLastInsertID: integer;
begin
// datResult = TADODataSet
datResult.Active := False;
datResult.CommandText := 'select @@IDENTITY as [ID]';
datResult.Active := True;
Result := datResult.FieldByName('id').AsInteger;
datResult.Active := False;
end;
Now before getting the last inserted record record id = autoincrement field, in other words calling the above function. You have to do a SQL insert like the following
procedure InsertRec;
begin
// datCommand = TADOCommand
datCommand.CommandText := 'insert into [test] ( [name] ) values ( "Test" )';
datCommand.Execute;
end;
Now if we like to know which is the last autoinc value ( notice that the getlastinsertid proc. only works after the insertrec proc)
procedure Test;
begin
InsertRec;
Showmessage(format('lastinsertid : %d', [GetLastInsertID]));
end;
Hope you can make this work, it works for me, any questions feel free to ask
Feliratkozás:
Bejegyzések (Atom)