2005. április 30., szombat
How to count the number of ones in a binary word
Problem/Question/Abstract:
How to count the number of ones in a binary word
Answer:
This was written (but not necessarily invented) by Paul King. The algorithm counts the number of ones in a binary word. You could of course just look at each bit and count how many of them are ones. But that takes as many cycles as the number of bits in a word. This clever algorithm takes as many cycles as the number of ones in the word, so on average is faster (unless all your data consists of words in which all the bits are ones). I don't know if it is well known, but I have frequently shown it to younger programmers, who are always surprised by it.
I only ever saw the algorithm in assembler, but as a recursive Pascal function it would be something like this. (Asssuming that X and Y gives the bitwise 'and' of X and Y).
function CountBits(X: word): integer;
{return the number of bits that are ones in X}
begin
if (x = 0) then
CountBits := 0
else
CountBits := 1 + CountBits(X and (X - 1));
end;
This works because if X is not zero, (X and (X-1)) always has exactly one fewer ones in it than X does.
Of course making it recursive would slow it down, so I suppose the non-recursive version would be better. Something like this:
function CountBits(X: word): integer;
{return number of bits that are ones in X}
var
temp: word;
result: integer;
begin
temp := X;
result := 0;
while temp < > 0 do
begin
result := result + 1;
temp := (temp and (temp - 1));
end;
CountBits := result;
end;
Your non-recursive implementation can be optimized a bit further. Modern (Delphi) Pascals have a built-in "result" variable within functions. Beyond other benefits, it makes renaming functions easier, too. No hunting through for other internal name references.
Also: If you're not making the parameter a const parameter, I find it easier to read (and more efficient?) to just use the parameter in the calculations. (No need for the temp var.) Delphi would probably optimize that immediately to a register variable/ parameter, too. I also expanded the parameter and temp var. to an integer; words are only 16 bits. Integers will "grow" with the compiler and available hardware support to the largest comfortably-handled integer size within the environment.
So you'd end up with something like:
function CountBits(X: integer): integer;
{return number of bits that are ones in X}
begin
result := 0;
while x < > 0 do
begin
inc(result);
X := (X and (X - 1));
end;
end;
I've seen this before, but it smacks of "tricks" (which I avoid) so I fear that when I needed it I did it the obvious way by a preprepared table:
function countbits(n: cardinal): integer;
const
bytebits: array[0..255] of byte = (0, 1, 1, 2, 1, 2, 2, 3, 1...);
{these values generated by your program??}
begin
result := 0;
repeat
inc(result, bytebits[n and $FF];
n := n shr 8;
until
n = 0;
end;
Should be a lot faster and you can trade off storage against speed by using a 4-bit, 8-bit, 12-bit
or 16-bit initial array.
2005. április 29., péntek
Microsoft Access '97 password
Problem/Question/Abstract:
Get the MicroSoft Access '97 password.
Answer:
function GetMDB97PassWord(Filename: string): string;
const
XorArr: array[0..12] of Byte = ($86, $FB, $EC, $37, $5D, $44, $9C, $FA, $C6, $5E,
$28, $E6, $13);
var
I: Integer;
Arr: array[0..12] of Byte;
S1: string;
FI: file of Byte;
By: Byte;
Access97: Boolean;
FileError: Boolean;
begin
// Init
FileError := False;
Access97 := True;
// Open file
AssignFile(FI, Filename);
Reset(FI);
// Read file
I := 0;
repeat
if not Eof(FI) then
begin
Read(FI, By);
Inc(I);
end;
until (I = $42) or Eof(FI);
if Eof(FI) then
FileError := True;
// Read password string
for I := 0 to 12 do
if not Eof(FI) then
Read(FI, Arr[I]);
if Eof(FI) then
FileError := True;
//Close file
CloseFile(FI);
// Read string in S1
S1 := '';
for I := 0 to 12 do
S1 := S1 + Chr(Arr[I]);
// Is nul string?
if S1 = #0 + #0 + #0 + #0 + #0 + #0 + #0 + #0 + #0 + #0 + #0 + #0 + #0 then
Access97 := False;
// Decode string
S1 := '';
for I := 0 to 12 do
S1 := S1 + Chr(Arr[I] xor XORArr[I]);
// Find end of string
I := 0;
repeat
Inc(I);
until (S1[I] = #0) or (I = 14);
if I <= 14 then
S1 := Copy(S1, 1, I - 1);
// Gather the results
if Access97 then
begin
if Length(S1) > 0 then
Result := 'The password is: ' + S1 + '.'
else
Result := 'The file is NOT password protected.';
end
else
Result := 'The file is not an Access 97 file / wrong format.';
if FileError then
Result := 'File error';
end;
2005. április 28., csütörtök
How to position the cursor on the right-hand side of a TEdit
Problem/Question/Abstract:
I want to write an Edit component, but I want the cursor to stay on the right when the user types in new characters.
Answer:
procedure TForm1.Edit1Change(Sender: TObject);
begin
if IsChanging then
exit; {Avoid recursion}
IsChanging := true;
try
{Remove the first character}
Edit1.Text := copy(Edit1.Text, 2, length(Edit1.Text) - 1);
{And move the cursor to the end of the text}
Edit1.SelStart := length(Edit1.Text);
finally
IsChanging := false;
end;
end;
At design time (or in Create), put several spaces as Edit1.Text. They will be replaced one by one with characters typed by the user. IsChanging is a private variable of type boolean.
2005. április 27., szerda
Detect if Excel is installed
Problem/Question/Abstract:
In the application I am writing I need to get data from an Excel spreadsheet and then insert it into a database. I am going to automate Excel. What I would like to know is how I can detect to see if excel is installed, and if it is, what version of Excel is on the user's computer. How can I do this?
Answer:
{ ... }
var
ClassID: TCLSID;
strOLEObject: string;
begin
strOLEObject := 'Excel.Application';
if (CLSIDFromProgID(PWideChar(WideString(strOLEObject)), ClassID) = S_OK) then
begin
{application is installed}
end
else
begin
{application is not installed}
end
end;
To get the version, just read Version property of Excel.Application:
xls := CreateOLEObject('Excel.Application');
v := xls.Version;
2005. április 26., kedd
How can I put a button on a form's caption bar?
Problem/Question/Abstract:
I've seen some programs that add text or buttons on the title bar of a form. How can I do this in Delphi?
Answer:
Introduction
I got my first insight into solving this problem when I wrote a previous tip that covered rolling up the client area of forms so that only the caption bar showed. In my research for that tip, I came across the WMSetText message that is used for drawing on a form's canvas. I wrote a sample application to test drawing in the caption area. The only problem with my original code was that the button would disappear when I resized or moved the form.
I turned to Delphi/Pascal guru Neil Rubenking for help. He pointed me in the direction of his book, Delphi Programming Problem Solver, which contains an example for doing this exact thing. The code below is an adaptation of the example in his book. The most fundamental difference between our examples is that I wanted to make a speedbutton with a bitmap glyph, and Neil actually drew a shape directly on the canvas. He also placed the button created in 16-bit Delphi on the left-hand side of the frame, and Win32 button placement was on the right. I wanted my buttons to be placed on the right for both versions, so I wrote appropriate code to handle that. The deficiency in my code was the lack of handlers for activation and painting in the non-client area of the form.
One thing I'm continually discovering is that there is a very definitive structure in Windows &mdash a definite hierarchy of functions. I've realized that the thing that makes Windows programming at the API level difficult is the sheer number of functions in the API set. For those who are reluctant to dive into the WinAPI, think in terms of categories first, then narrow your search. You'll find that doing it this way will make your life much easier.
What makes all of this work is Windows messages. The messages we're interested in here are not the usual Windows messages handled by plain-vanilla Windows apps, but are specific to an area of a window called the non-client area. The client area of a window is the part inside the border where most applications present information. The non-client area consists of the window's borders, caption bar, system menu and sizing buttons. The Windows messages that pertain to this area have the naming convention of WM_NCMessageType. Taking the name apart, 'WM' stands for Windows Message, 'NC' stands for Non-client area, and MessageType is the type of message being trapped. For example, WM_NCPaint is the paint message for the non-client area. Taking into account the hierarchical and categorical nature of the Windows API, nomenclature is a very big part of it; especially with Windows messages. If you look in the help file under messages, peruse through the list of messages and you will see the order that is followed.
Let's look at a list of things that we need to consider to add a button to the title bar of a form:
We need to have a function to draw the button.
We'll have to trap drawing and painting events so that our button stays visible when the form activates, resizes or moves.
We're dropping a button on the title bar, so we have to have a way of trapping for a mouse click on the button.
I'll now discuss these topics, in the above order.
Drawing a TRect as a Button
You can't drop VCL objects onto a non-client area of a window, but you can draw on it and simulate the appearance of a button. In order to perform drawing in the title bar of a window, you have to do three very important things, in order:
You must get the current measurements of the window and the size of the frame bitmaps so you know what area to draw in and how big to draw the rectangle.
Then you have to define a TRect structure with the proper size and position within the title bar.
Finally, you have to draw the TRect to appear as a button, then add any glyphs or text you might want to draw to the buttonface.
All of this is accomplished in a single call. For this program we make a call to the DrawTitleButton procedure, which is listed below:
procedure TTitleBtnForm.DrawTitleButton;
var
bmap: TBitmap; {Bitmap to be drawn - 16 X 16 : 16 Colors}
XFrame, {X and Y size of Sizeable area of Frame}
YFrame,
XTtlBit, {X and Y size of Bitmaps in caption}
YTtlBit: Integer;
begin
{Get size of form frame and bitmaps in title bar}
XFrame := GetSystemMetrics(SM_CXFRAME);
YFrame := GetSystemMetrics(SM_CYFRAME);
XTtlBit := GetSystemMetrics(SM_CXSIZE);
YTtlBit := GetSystemMetrics(SM_CYSIZE);
{$IFNDEF WIN32}
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2);
{$ELSE} {Delphi 2.0 positioning}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2)
else
TitleButton := Bounds(Width - XFrame - 4 * XTtlBit + 2,
XFrame + 2,
XTtlBit + 2,
YTtlBit + 2);
{$ENDIF}
Canvas.Handle := GetWindowDC(Self.Handle); {Get Device context for drawing}
try
{Draw a button face on the TRect}
DrawButtonFace(Canvas, TitleButton, 1, bsAutoDetect, False, False, False);
bmap := TBitmap.Create;
bmap.LoadFromFile('help.bmp');
with TitleButton do
{$IFNDEF WIN32}
Canvas.Draw(Left + 2, Top + 2, bmap);
{$ELSE}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
Canvas.Draw(Left + 2, Top + 2, bmap)
else
Canvas.StretchDraw(TitleButton, bmap);
{$ENDIF}
finally
ReleaseDC(Self.Handle, Canvas.Handle);
bmap.Free;
Canvas.Handle := 0;
end;
end;
Step 1 above is accomplished by making four calls to the WinAPI function GetSystemMetrics, asking the system for the width and height of the window that can be sized (SM_CXFRAME and SM_CYFRAME), and the size of the bitmaps contained on the title bar (SM_CXSIZE and SM_CYSIZE).
Step 2 is performed with the Bounds function, which returns a TRect defined by the size and position parameters that are supplied to it. Notice that I used some conditional compiler directives here. This is because the size of the title bar buttons in Windows 95 and Windows 3.1 are different, so they have to be sized differently. And since I wanted to be able to compile this in either version of Windows, I used a test for the predefined symbol, WIN32, to see which version of Windows the program is compiled under. However, since the Windows NT UI is the same as Windows 3.1, it's necessary to grab further version information under the Win32 conditional to see if the Windows version is Windows NT. If so, we define the TRect to be just like the Windows 3.1 TRect.
To perform Step 3, we make a call to the Buttons unit's DrawButtonFace to draw button features within the TRect that we defined. As added treat, I included code to draw a bitmap in the button. You'll see that I used a conditional compiler directive to draw the bitmap under different versions of Windows. I did this because the bitmap I used was 16x16 pixels, which might be too big for Win95 buttons. So I used StretchDraw under Win32 to stretch the bitmap to the size of the button.
Trapping the Drawing and Painting Events
You must make sure that the button will stay visible every time the form repaints itself. Painting occurs in response to activation and resizing, which fire off paint and text setting messages that will redraw the form. If you don't have a facility to redraw your button, you'll lose it every time a repaint occurs. So what we have to do is write event handlers which will perform their default actions and redraw our button when they fire off. The following four procedures handle the paint triggering and painting events:
{Paint triggering events}
procedure TForm1.WMNCActivate(var Msg: TWMNCActivate);
begin
inherited;
DrawTitleButton;
end;
procedure TForm1.FormResize(Sender: TObject);
begin
Perform(WM_NCACTIVATE, Word(Active), 0);
end;
{Painting events}
procedure TForm1.WMNCPaint(var Msg: TWMNCPaint);
begin
inherited;
DrawTitleButton;
end;
procedure TForm1.WMSetText(var Msg: TWMSetText);
begin
inherited;
DrawTitleButton;
end;
Every time one of these events fires off, it makes a call to the DrawTitleButton procedure. This will ensure that our button is always visible on the title bar. Notice that we use the default handler OnResize on the form to force it to perform a WM_NCACTIVATE.
Handling Mouse Clicks
Now that we've got code that draws our button and ensures that it's always visible, we have to handle mouse clicks on the button. The way we do this is with two procedures. The first procedure tests to see if the mouse click was in the area of our button, then the second procedure actually performs the code execution associated with our button. Let's look at the code:
{Mouse-related procedures}
procedure TForm1.WMNCHitTest(var Msg: TWMNCHitTest);
begin
inherited;
{Check to see if the mouse was clicked in the area of the button}
with Msg do
if PtInRect(TitleButton, Point(XPos - Left, YPos - Top)) then
Result := htTitleBtn;
end;
procedure TForm1.WMNCLButtonDown(var Msg: TWMNCLButtonDown);
begin
inherited;
if (Msg.HitTest = htTitleBtn) then
ShowMessage('You pressed the new button');
end;
The first procedure WMNCHitTest(var Msg : TWMNCHitTest) is a hit tester message to determine where the mouse was clicked in the non-client area. In this procedure we test if the point defined by the message was within the bounds of our TRect by using the PtInRect function. If the mouse click was performed in the TRect, then the result of our message is set to htTitleBtn, which is a constant that was declared as htSizeLast + 1. htSizeLast is a hit test constant generated by hit test events to test where the last hit occurred.
The second procedure is a custom handler for a left mouse click on a button in the non-client area. Here we test if the hit test result was equal to htTitleBtn. If it is, we show a message. You can make any call you choose to at this point.
Putting it All Together
Let's look at the entire code in the form to see how it all works together:
unit Capbtn;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Buttons;
type
TTitleBtnForm = class(TForm)
procedure FormResize(Sender: TObject);
private
TitleButton: TRect;
procedure DrawTitleButton;
{Paint-related messages}
procedure WMSetText(var Msg: TWMSetText); message WM_SETTEXT;
procedure WMNCPaint(var Msg: TWMNCPaint); message WM_NCPAINT;
procedure WMNCActivate(var Msg: TWMNCActivate); message WM_NCACTIVATE;
{Mouse down-related messages}
procedure WMNCHitTest(var Msg: TWMNCHitTest); message WM_NCHITTEST;
procedure WMNCLButtonDown(var Msg: TWMNCLButtonDown); message WM_NCLBUTTONDOWN;
function GetVerInfo: DWORD;
end;
var
TitleBtnForm: TTitleBtnForm;
const
htTitleBtn = htSizeLast + 1;
implementation
{$R *.DFM}
procedure TTitleBtnForm.DrawTitleButton;
var
bmap: TBitmap; {Bitmap to be drawn - 16 X 16 : 16 Colors}
XFrame, {X and Y size of Sizeable area of Frame}
YFrame,
XTtlBit, {X and Y size of Bitmaps in caption}
YTtlBit: Integer;
begin
{Get size of form frame and bitmaps in title bar}
XFrame := GetSystemMetrics(SM_CXFRAME);
YFrame := GetSystemMetrics(SM_CYFRAME);
XTtlBit := GetSystemMetrics(SM_CXSIZE);
YTtlBit := GetSystemMetrics(SM_CYSIZE);
{$IFNDEF WIN32}
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2);
{$ELSE} {Delphi 2.0 positioning}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2)
else
TitleButton := Bounds(Width - XFrame - 4 * XTtlBit + 2,
XFrame + 2,
XTtlBit + 2,
YTtlBit + 2);
{$ENDIF}
Canvas.Handle := GetWindowDC(Self.Handle); {Get Device context for drawing}
try
{Draw a button face on the TRect}
DrawButtonFace(Canvas, TitleButton, 1, bsAutoDetect, False, False, False);
bmap := TBitmap.Create;
bmap.LoadFromFile('help.bmp');
with TitleButton do
{$IFNDEF WIN32}
Canvas.Draw(Left + 2, Top + 2, bmap);
{$ELSE}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
Canvas.Draw(Left + 2, Top + 2, bmap)
else
Canvas.StretchDraw(TitleButton, bmap);
{$ENDIF}
finally
ReleaseDC(Self.Handle, Canvas.Handle);
bmap.Free;
Canvas.Handle := 0;
end;
end;
{Paint triggering events}
procedure TTitleBtnForm.WMNCActivate(var Msg: TWMNCActivate);
begin
inherited;
DrawTitleButton;
end;
procedure TTitleBtnForm.FormResize(Sender: TObject);
begin
Perform(WM_NCACTIVATE, Word(Active), 0);
end;
{Painting events}
procedure TTitleBtnForm.WMNCPaint(var Msg: TWMNCPaint);
begin
inherited;
DrawTitleButton;
end;
procedure TTitleBtnForm.WMSetText(var Msg: TWMSetText);
begin
inherited;
DrawTitleButton;
end;
{Mouse-related procedures}
procedure TTitleBtnForm.WMNCHitTest(var Msg: TWMNCHitTest);
begin
inherited;
{Check to see if the mouse was clicked in the area of the button}
with Msg do
if PtInRect(TitleButton, Point(XPos - Left, YPos - Top)) then
Result := htTitleBtn;
end;
procedure TTitleBtnForm.WMNCLButtonDown(var Msg: TWMNCLButtonDown);
begin
inherited;
if (Msg.HitTest = htTitleBtn) then
ShowMessage('You pressed the new button');
end;
function TTitleBtnForm.GetVerInfo: DWORD;
var
verInfo: TOSVERSIONINFO;
begin
verInfo.dwOSVersionInfoSize := SizeOf(TOSVersionInfo);
if GetVersionEx(verInfo) then
Result := verInfo.dwPlatformID;
{Returns:
VER_PLATFORM_WIN32s Win32s on Windows 3.1
VER_PLATFORM_WIN32_WINDOWS Win32 on Windows 95
VER_PLATFORM_WIN32_NT Windows NT }
end;
end.
Suggestions for Exploring
You might want to play around with this code a bit to customize it to your own needs. For instance, if you want to add a bigger button, add pixels to the XTtlBit var. You can also mess around with creating a floating toolbar that is purely on the title bar. Also, now that you have a means of interrogating what's going on in the non-client area of the form, you might want to play around with the default actions taken with the other buttons like the System Menu button to perhaps display your own custom menu.
Take heed, though: Playing around with Windows messages can be dangerous. Save your work constantly, and be prepared for some system crashes while you experiment.
2005. április 25., hétfő
Getting a list of installed services
Problem/Question/Abstract:
Want to get a list of active, inactive or all Windows services?
Answer:
Following function can help you to do this, but be sure to read other Windows services related tips for details.
const
//
// Service Types
//
SERVICE_KERNEL_DRIVER = $00000001;
SERVICE_FILE_SYSTEM_DRIVER = $00000002;
SERVICE_ADAPTER = $00000004;
SERVICE_RECOGNIZER_DRIVER = $00000008;
SERVICE_DRIVER =
(SERVICE_KERNEL_DRIVER or
SERVICE_FILE_SYSTEM_DRIVER or
SERVICE_RECOGNIZER_DRIVER);
SERVICE_WIN32_OWN_PROCESS = $00000010;
SERVICE_WIN32_SHARE_PROCESS = $00000020;
SERVICE_WIN32 =
(SERVICE_WIN32_OWN_PROCESS or
SERVICE_WIN32_SHARE_PROCESS);
SERVICE_INTERACTIVE_PROCESS = $00000100;
SERVICE_TYPE_ALL =
(SERVICE_WIN32 or
SERVICE_ADAPTER or
SERVICE_DRIVER or
SERVICE_INTERACTIVE_PROCESS);
uses WinSvc;
//-------------------------------------
// Get a list of services
//
// return TRUE if successful
//
// sMachine:
// machine name, ie: \\SERVER
// empty = local machine
//
// dwServiceType
// SERVICE_WIN32,
// SERVICE_DRIVER or
// SERVICE_TYPE_ALL
//
// dwServiceState
// SERVICE_ACTIVE,
// SERVICE_INACTIVE or
// SERVICE_STATE_ALL
//
// slServicesList
// TStrings variable to storage
//
function ServiceGetList(
sMachine: string;
dwServiceType,
dwServiceState: DWord;
slServicesList: TStrings)
: boolean;
const
//
// assume that the total number of
// services is less than 4096.
// increase if necessary
cnMaxServices = 4096;
type
TSvcA = array[0..cnMaxServices]
of TEnumServiceStatus;
PSvcA = ^TSvcA;
var
//
// temp. use
j: integer;
//
// service control
// manager handle
schm: SC_Handle;
//
// bytes needed for the
// next buffer, if any
nBytesNeeded,
//
// number of services
nServices,
//
// pointer to the
// next unread service entry
nResumeHandle: DWord;
//
// service status array
ssa: PSvcA;
begin
Result := false;
// connect to the service
// control manager
schm := OpenSCManager(
PChar(sMachine),
nil,
SC_MANAGER_ALL_ACCESS);
// if successful...
if (schm > 0) then
begin
nResumeHandle := 0;
New(ssa);
EnumServicesStatus(
schm,
dwServiceType,
dwServiceState,
ssa^[0],
SizeOf(ssa^),
nBytesNeeded,
nServices,
nResumeHandle);
//
// assume that our initial array
// was large enough to hold all
// entries. add code to enumerate
// if necessary.
//
for j := 0 to nServices - 1 do
begin
slServicesList.
Add(StrPas(
ssa^[j].lpDisplayName));
end;
Result := true;
Dispose(ssa);
// close service control
// manager handle
CloseServiceHandle(schm);
end;
end;
To get a list of all Windows services into a listbox named ListBox1:
ServiceGetList('', SERVICE_WIN32, SERVICE_STATE_ALL, ListBox1.Items);
2005. április 24., vasárnap
Delphi IDE freezes every 5 minutes for one minute
Problem/Question/Abstract:
My Delphi IDE freezes every five or six minutes for approximately 50 seconds. While the IDE is frozen, I can switch to any other application – they all respond normally. The IDE is completely unresponsive during the freeze, for example it does not repaint its windows. After the 50-second-freeze the IDE behaves fine for five minutes until it freezes again. This effect was seen on an installation of the demo version of Delphi 5.
Answer:
As a first guess, I would turn off Delphi Direct. Do you have a permanent Internet connection? Maybe something is configured wrong and Delphi tries repeatedly to connect to www.Borland.com. You can turn off Delphi Direct in Tools | Environment Options
The freeze may occur because you have a bad directory in your (library) search path. Check all directories in your paths and make sure that all drives are actually mapped. The 50-second-freeze sounds like Windows tries to open a network connection. It could also be that a directory/ drive name points to a CD-ROM now and there is no CD in it or it contains a different CD than expected. Trim your paths to what is needed.
You may want to find out what is installed in your IDE that makes it searches your path periodically. I recommend de-activating as many packages as you can. This is done via Components | Packages.
The fact that it is a demo version should not be relevant. Good luck!
2005. április 23., szombat
How to handle multiple, simultaneous key presses
Problem/Question/Abstract:
If I want to write a simple game where you can move and shoot at the same time, what would be the best way of handling the multiple key presses?
Answer:
unit KeysForm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls;
type
TForm1 = class(TForm)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
procedure FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
procedure FormCreate(Sender: TObject);
procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure FormMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
Keys: array[Byte] of Boolean;
implementation
{$R *.DFM}
procedure TForm1.Timer1Timer(Sender: TObject);
var
KeyNum, c: Integer;
begin
for KeyNum := 0 to 255 do
begin
c := Ord(Keys[KeyNum]) * 255;
Canvas.Pixels[KeyNum * 2, 10] := RGB(c, c, c);
Canvas.Pixels[KeyNum * 2 + 1, 10] := RGB(c, c, c);
Canvas.Pixels[KeyNum * 2 + 1, 11] := RGB(c, c, c);
Canvas.Pixels[KeyNum * 2, 11] := RGB(c, c, c);
end;
end;
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
Keys[Key] := True;
Key := 0;
end;
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
Keys[Key] := False;
Key := 0;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
KeyNum: Integer;
begin
for KeyNum := 0 to 255 do
Keys[KeyNum] := False;
end;
procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if (Button = mbLeft) then
Keys[VK_LBUTTON] := True
else if (Button = mbMiddle) then
Keys[VK_MBUTTON] := True
else if (Button = mbRight) then
Keys[VK_RBUTTON] := True;
end;
procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if (Button = mbLeft) then
Keys[VK_LBUTTON] := False
else if (Button = mbMiddle) then
Keys[VK_MBUTTON] := False
else if (Button = mbRight) then
Keys[VK_RBUTTON] := False;
end;
end.
2005. április 22., péntek
How to intercept a click on the forms' minimize button
Problem/Question/Abstract:
Does anybody know how to capture the minimize button press and act on it before it actually minimizes the form?
Answer:
You should intercept WM_SYSCOMMAND messages like this:
unit Unit1;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
public
procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.WMSysCommand;
begin
if (Msg.CmdType = SC_MINIMIZE) or (Msg.CmdType = SC_MAXIMIZE) then
MessageBeep(0);
DefaultHandler(Msg);
end;
end.
2005. április 21., csütörtök
Credit Card Validation (2)
Problem/Question/Abstract:
How can I know if a number is a valid credit card number?
Answer:
The following unit can be used to make credit card verification
unit Creditc;
{*****************************************************************************
Credit Card Number Validator Unit for Delphi
Version: 1.1
Date: December 20, 1996
This unit is based on the public domain program ccard by Peter Miller.
It is released to the public for free of charge use, but the author
reserves all rights.
copyright 1996 by Shawn Wilson Harvell ( shawn@inet.net )
usage:
Add this unit to the uses clause of any unit that needs access to the
validation function.
IsValidCreditCardNumber( CardNumber, ReturnMessage ) returns Boolean
for example, use it in an if statement that Messages user if invalid.
CardNumber is a string containing the number that you want to validate
ReturnMessage is a string where the function can place any messages it
may return ( meaning that it will overwrite whatever is in it )
returns true if valid, false otherwise.
dashes and space in the input value are taken care of by the function,
if other characters are possible, you may wish to remove them as well.
The function RemoveChar will take care of this quite easily, simply
pass the input string and the char you wish to delete.
Users are free to modify this unit for their own use, but in
distributing you should advise all users of the changes made.
Use this unit at your own risk, it does not come with any warranties
either express or implied. Damages resulting from the use of this
unit are the sole responsibility of the user.
This should work as is for Delphi versions 1 and 2, some slight
modifications may be necessary for Turbo Pascal ( mainly due to use to
conversion functions from the SysUtils unit ).
If you do find this useful, have any comments or suggestions, please
drop the author an email at shawn@inet.net
Revision History
version 1.1 -- December 20, 1996
blooper with Discover cards, added their length mask to the "database"
version 1.0 -- October 26, 1996
initial release
*****************************************************************************}
interface
uses SysUtils;
function IsValidCreditCardNumber(CardNumber: string; var MessageText: string): Boolean;
implementation
const
CardPrefixes: array[1..19] of string =
('2014', '2149', '300', '301', '302',
'303', '304', '305', '34', '36', '37',
'38', '4', '51', '52', '53', '54', '55', '6011');
CardTypes: array[1..19] of string =
('enRoute',
'enRoute',
'Diner Club/Carte Blanche',
'Diner Club/Carte Blanche',
'Diner Club/Carte Blanche',
'Diner Club/Carte Blanche',
'Diner Club/Carte Blanche',
'Diner Club/Carte Blanche',
'American Express',
'Diner Club/Carte Blanche',
'American Express',
'Diner Club/Carte Blanche',
'Visa',
'MasterCard',
'MasterCard',
'MasterCard',
'MasterCard',
'MasterCard',
'Discover');
function RemoveChar(const Input: string; DeletedChar: Char): string;
var
Index: Word; { counter variable }
begin
{ all this function does is iterate through string looking for char, if found }
{ it deletes it }
Result := Input;
for Index := Length(Result) downto 1 do
if Result[Index] = DeletedChar then
Delete(Result, Index, 1);
end;
function ShiftMask(Input: Integer): Integer;
begin
{ simply a wrapper for this left bit shift operation }
result := (1 shl (Input - 12));
end;
function ConfirmChecksum(CardNumber: string): Boolean;
var
CheckSum: Integer; { Holds the value of the operation }
Flag: Boolean; { used to indicate when ready }
Counter: Integer; { index counter }
PartNumber: string; { used to extract each digit of number }
Number: Integer; { used to convert each digit to integer }
begin
{**************************************************************************
This is probably the most confusing part of the code you will see, I know
that it is some of the most confusing I have ever seen. Basically, this
function is extracting each digit of the number and subjecting it to the
checksum formula established by the credit card companies. It works from
the end to the front.
**************************************************************************}
{ get the starting value for our counter }
Counter := Length(CardNumber);
CheckSum := 0;
PartNumber := '';
Number := 0;
Flag := false;
while (Counter >= 1) do
begin
{ get the current digit }
PartNumber := Copy(CardNumber, Counter, 1);
Number := StrToInt(PartNumber); { convert to integer }
if (Flag) then { only do every other digit }
begin
Number := Number * 2;
if (Number >= 10) then
Number := Number - 9;
end;
CheckSum := CheckSum + Number;
Flag := not (Flag);
Counter := Counter - 1;
end;
result := ((CheckSum mod 10) = 0);
end;
function GetMask(CardName: string): Integer;
begin
{ the default case }
result := 0;
if (CardName = 'MasterCard') then
result := ShiftMask(16);
if (CardName = 'Visa') then
result := (ShiftMask(13) or ShiftMask(16));
if (CardName = 'American Express') then
result := ShiftMask(15);
if (CardName = 'Diner Club/Carte Blanche') then
result := ShiftMask(14);
if (CardName = 'Discover') then
result := ShiftMask(16);
end;
function IsValidCreditCardNumber(CardNumber: string; var MessageText: string):
Boolean;
var
StrippedNumber: string; { used to hold the number bereft of extra chars }
Index: Integer; { general purpose counter for loops, etc }
TheMask: Integer; { number we will use for the mask }
FoundIt: Boolean; { used to indicate when something is found }
CardName: string; { stores the name of the type of card }
PerformChecksum: Boolean; { the enRoute type of card doesn't get it }
begin
{ first, get rid of spaces, dashes }
StrippedNumber := RemoveChar(CardNumber, ' ');
StrippedNumber := RemoveChar(StrippedNumber, '-');
{ if the string was zero length, then OK too }
if (StrippedNumber = '') then
begin
result := true;
exit;
end;
{ initialize return variables }
MessageText := '';
result := true;
{ set our flag variable }
FoundIt := false;
{ check for invalid characters right off the bat }
for Index := 1 to Length(StrippedNumber) do
begin
case StrippedNumber[Index] of
'0'..'9': FoundIt := FoundIt; { non op in other words }
else
MessageText := 'Invalid Characters in Input';
result := false;
exit;
end;
end;
{ now let's determine what type of card it is }
for Index := 1 to 19 do
begin
if (Pos(CardPrefixes[Index], StrippedNumber) = 1) then
begin
{ we've found the right one }
FoundIt := true;
CardName := CardTypes[Index];
TheMask := GetMask(CardName);
end;
end;
{ if we didn't find it, indicates things are already ary }
if (not FoundIt) then
begin
CardName := 'Unknown Card Type';
TheMask := 0;
MessageText := 'Unknown Card Type ';
result := false;
exit;
end;
{ check the length }
if ((Length(StrippedNumber) > 28) and result) then
begin
MessageText := 'Number is too long ';
result := false;
exit;
end;
{ check the length }
if ((Length(StrippedNumber) < 12) or
((shiftmask(length(strippednumber)) and themask) = 0)) then
begin
messagetext := 'number length incorrect';
result := false;
exit;
end;
{ check the checksum computation }
if (cardname = 'enroute') then
performchecksum := false
else
performchecksum := true;
if (performchecksum and (not confirmchecksum(strippednumber))) then
begin
messagetext := 'bad checksum';
result := false;
exit;
end;
{ if result is still true, then everything is ok }
if (result) then
messagetext := 'number ok: card type: ' + cardname;
{ if the string was zero length, then ok too }
if (strippednumber = '') then
result := true;
end;
end.
2005. április 20., szerda
Creating a system tray application
Problem/Question/Abstract:
How I can make my application not appear on the main display and but just in the system tray on startup?
Answer:
You could use RxLib (freeware component collection) - it contains a component that does this. Drop the RxTrayIcon component on your main form and minimize/ hide your application with this code:
ShowWindow(Application.Handle, SW_HIDE);
Application.Minimize;
If using RxLib is not an option, then you can build it yourself with the ShellAPI function Shell_NotifyIcon(). Use the application from below as a starting point.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, Menus, ShellAPI, ExtCtrls;
type
TForm1 = class(TForm)
PopupMenu1: TPopupMenu;
Open1: TMenuItem;
Exit1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure Open1Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ private declarations }
procedure WndProc(var Msg: TMessage); override;
public
{ public declarations }
IconData: TNotifyIconData;
IconCount: integer;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.WndProc(var Msg: TMessage);
var
aPoint: TPoint;
begin
case Msg.Msg of
WM_USER + 1:
case Msg.lParam of
WM_RBUTTONDOWN:
begin
SetForegroundWindow(Handle);
GetCursorPos(aPoint);
PopupMenu1.Popup(aPoint.x, aPoint.y);
PostMessage(Handle, WM_NULL, 0, 0);
end
end;
end;
inherited;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
BorderIcons := [biSystemMenu];
IconCount := 0;
IconData.cbSize := sizeof(IconData);
IconData.Wnd := Handle;
IconData.uID := 100;
IconData.uFlags := NIF_MESSAGE + NIF_ICON + NIF_TIP;
IconData.uCallbackMessage := WM_USER + 1;
IconData.hIcon := Application.Icon.Handle;
StrPCopy(IconData.szTip, Application.Title);
Shell_NotifyIcon(NIM_ADD, @IconData);
end;
procedure TForm1.Open1Click(Sender: TObject);
begin
Form1.Show;
ShowWindow(Application.Handle, SW_HIDE);
end;
procedure TForm1.Exit1Click(Sender: TObject);
begin
Shell_NotifyIcon(NIM_DELETE, @IconData);
Application.ProcessMessages;
Application.Terminate;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caNone;
Form1.Hide;
end;
begin
ShowWindow(Application.Handle, SW_HIDE);
end;
2005. április 19., kedd
DSOAP, ADO and XML
Problem/Question/Abstract:
DSOAP, ADO and XML
Answer:
Introduction
Developing distributed applications is a big shift from the client/server world. The traditional client/server model is based on the assumption that a client application can open a database connection and hold it until the task is terminated. While this approach simplifies the task of programmers, it tremendously impacts scalability and maintenance. There are multiple reasons for this: Hardware is a finite resource: for the big a server can be, it's very easy to terminate its resources as the number of connected and concurrent clients increase. Database licenses are expensive and for each client a license is being used. It is not easy if not impossible in some situations to effectively load balance the database tier. Finally, by using a two tier model, business rules are coded in either the client or the database via stored procedures. It is difficult to update these two tiers. Technologies such as COM+ and Corba or frameworks such as Java's J2EE and Microsoft .Net push towards the development of distributed systems and provide the necessary infostructure to build scalable multi-tier systems. Regardless of your needs (LAN or Internet) by developing a multi-tier system you will have a product that is much easier to update and scale (both up or out). Before jumping to the example, I want to reserve a few words to two very important aspects of efficient multi tier development: Client independence: If your middle tier (the one that contains business logic) is accessed by both web and desktop clients, you need to plan ahead and make sure it will work with both. Web clients are very different from desktop clients. The architecture of HTTP makes them implicitly stateless and there's no preservation of state between an HTML page and the other. Internet Information Server has a Session object that can help you storing values of any type (simple data types as well as COM objects) and keeping them until the user keeps his/her browser open. Unfortunately, if you have a cluster of webservers (web farm), this information is not propagated across the servers. Using IIS Session object is not a good idea in a clustered environment. You will need to use other techniques to store client's state. You need to design your middle tier using patterns that do not depend on any specific capability of the clients that will use it. Preferabily you should conform to the lowest denominator which is the web client. I will cover this topic in another article but for now let's continue with the other key requirement. Openness: In a world of web services like the one we are moving towards you don't know who or what will access your system. This is another reason for being client independent but it creates another requirement for successful multi-tier development: when facing the outside world, you need to use open standards that are globally accepted and used. Do not expect your clients will be able to use ADO Recordsets or communicate via Corba. The standards are represented today by SOAP and XML.
The example
The following example illustrates how to retrieve data from the Northwind sample database included in SQL Server. We will create a business object that queries the database using ADO and generates an XML streams that is used by the client for data presentation/manipulation. On the client we will access this XML stream using a common ADO Recordset (I will use the TADODataset included in ADOExpress). Finally, when we are done modifiying the data we will convert the recordset in XML and send it back to the business object. Finally, this will update the database. All this will be done using SOAP. The XML that ADO generates is perfectly usable from any type client. A Java client could use it as well after converting it into a format it's more convenient.
How to install the sample
In my previous article DSOAP Toolkit I explained how you create a COM object and how you expose this to the world using the Microsoft SOAP SDK. In this article I will focus on the implementation of the business object. I will show how you can convert an ADO Recordset in XML and vice versa. I incuded the WSDL and WSML files in the zip so you will not need to recreate them. Download the example, register the ActiveX library DSOAPXMLLib.dll and move the WSDL and WSML files under c:\inetpub\wwwroot. When you are done with this, open the client application and test the webservice by pressing the "Get Customers" button. I assume your computer has SQL Server installed. If SQL Server is installed on a remote machine, modify the constant DBConnStr in the unit uCustomersDataObject_Impl.pas before registering the ActiveX library and launcing the client application. If you need to change the folder where the WSDL file will be contained, make sure you update the URL specified at the end of it top match the new directory.
The business object
The ActiveX library contains one COM object called CustomersDataObject. Its interface contains only two methods (GetCustomers and UpdateCustomers) as shown below:
GetCustomers is declared as:
function GetCustomers(const aFilter: WideString; out Errors: WideString): WideString;
The "aFilter" parameter takes a valid SQL WHERE condition such as "CustomerID line 'A%' " without the WHERE keyword. You will use this to filter the amount of data you want to receive. The method returns the XML generated by an ADO Recordset. UpdateCustomers is declared as:
function UpdateCustomers(const someChanges: WideString; out Errors: WideString):
WordBool;
The parameter someChanges takes the delta of changes that have been committed on the client and updates the database. It will return TRUE if succesful or FALSE if it failed. Notice the out parameter Errors in both methods. This is not required in order to handle error notification. As I explained in my other article Using SOAP with Delphi, the protocol defines a standard way to report errors. If an exception would be raise on the client, the Microsoft SOAP SDK would trap it and encode it in a SOAP error condition which would then be reraised client side. Sometimes I prefer to trap everything myself. By doing this you could log every error in a database table and have more control over things. Still, leaving the exception unhandled, is a valid approach as well.
Streaming a Recordset in XML
Streaming a Recordset in XML is a matter of calling the Recordset.Save method passing an object that implements IStream as parameter. You can use the ADO Stream or the XMLDOMDocument object for this porpouse. The second one is generally faster because it uses UTF8 encoding (1 byte character) and is optimized for XML processing. The ADO Stream instead works with UTF16 encoding (try saving both XML streams to a file and see the difference in size. I realized this thanks to the comments of Shiv Kumar. Have a look at his site at http://www.matlus.com In the unit uADOCommon.pas you will find the following function that, given a connection string and a SQL command, creates a Recordset and converts it into XML:
function SQLToXML(const aConnection: OleVariant; const aSQLCommand: string): string;
var
rs: _Recordset;
xml: IXMLDOMDocument2;
begin
// Creates a disconnected recordset that will be streamed into XML
rs := CoRecordset.Create;
rs.CursorLocation := adUseClient;
rs.Open(aSQLCommand, aConnection, adOpenForwardOnly, adLockBatchOptimistic, 0);
rs.Set_ActiveConnection(nil);
// Streams _Recordset into XML
xml := CoDOMDocument30.Create;
rs.Save(xml, adPersistXML);
rs.Close;
result := xml.xml;
end;
Pay special attention to the line in which I call the Recordset's Open method. I specified adOpenForwardOnly as cursor type. This has an immediate effect on performances while querying data from a SQL database. Generally speaking, the more flexible the cursor you open, the slower and more resource intensive it will be. Take a look at the article SQL Server Cursor library if you want to know more about the subject. Another important thing I did in the function above is to set the Recordset's connection to NIL and the CursorLocation to adUseClient. When using this combination of values you will generate a disconnected, client side Recordset. The resulting XML will look like this:
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
<s:Schema id="RowsetSchema">
<s:ElementType name="row" content="eltOnly" rs:updatable="true">
<s:AttributeType name="CustomerID" rs:number="1" rs:writeunknown="true" rs:basecatalog="Northwind" rs:basetable="CUSTOMERS" rs:basecolumn="CustomerID" rs:keycolumn="true">
<s:datatype dt:type="string" dt:maxLength="5" rs:fixedlength="true" rs:maybenull="false"/>
</s:AttributeType>
<s:AttributeType name="CompanyName" rs:number="2" rs:writeunknown="true" rs:basecatalog="Northwind" rs:basetable="CUSTOMERS" rs:basecolumn="CompanyName">
<s:datatype dt:type="string" dt:maxLength="40" rs:maybenull="false"/>
</s:AttributeType>
[..]
<s:extends type="rs:rowbase"/>
</s:ElementType>
</s:Schema>
<rs:data>
<z:row CustomerID="ALFKI" CompanyName="Alfreds Futterkiste" ContactName="Maria Anders" ContactTitle="Sales Representative" Address="Obere Str. 57" City="Berlin" PostalCode="12209" Country="Germany" Phone="030-0074321" Fax="030-0076545"/>
<z:row CustomerID="ANATR" CompanyName="Ana Trujillo Emparedados y helados" ContactName="Ana Trujillo" ContactTitle="Owner" Address="Avda. de la Constituci�n 2222" City="M�xico D.F." PostalCode="05021" Country="Mexico" Phone="(5) 555-4729" Fax="(5) 555-3745"/>
[..]
</rs:data>
</xml>
Make it a WebService
What you need to do now is to register your ActiveX library and generate the correct WSDL files using the Microsoft SOAP SDK like I explained in my previous article DSOAP Toolkit. I included the correct WSDL files in the example and the only thing you need to do is to copy them in the root c:\inetpub\wwwroot.
The client
So far so good. We know how to get our data, how to convert it in XML and how to stream it to the client. What we need now is to a way to invoke all this functionality from the client. By using the DSOAP WSDL Import Wizard specifying "http://localhost/DSOAPXMLLib.WSDL" as source, you will be able to generate the unit uCustomersDataObject_Impl.pas. See my other article for details. In order to make the example a little bit more interesting, I decided to show how to feed a TADODataset with this XML stream. By doing this, you will still be able to use all your third party data aware components like it were a regular MIDAS Clientdataset or a BDE TTable. From inside the Delphi IDE the client will look like this:
Receiving data
In order to receive data you need to invoke the GetCustomers method and ceonvert the XML back into a Recordset. The function XMLToRecordset contained in the common unit uADOCommon.pas takes care of this conversion and returns a Recordset. Once you have that, just assign it to the ADODataset by setting its Recordset property. The following code is execute when you press the "Get Customers" button:
procedure TForm1.bGetCustomersClick(Sender: TObject);
var
errs: widestring;
xml: string;
rs: OleVariant;
begin
xml := fCustomersDataObject.GetCustomers(eFilter.Text, errs);
[...]
ADODataSet1.Recordset := XMLToRecordset(xml) as ADODB._Recordset;
[...]
end;
While this is the XMLToRecordset funtions' code:
function XMLToRecordset(const someXML: string): _Recordset;
var
xml: IXMLDOMDocument2;
rs: OleVariant;
begin
xml := CoDOMDocument30.Create;
xml.LoadXML(someXML);
rs := CoRecordset.Create;
rs.Open(xml);
result := IUnknown(rs) as _Recordset;
end;
By reversing what we did on the server side, we can feed the Recordset back with its rows. It almost cannot get simpler than this!
Sending updates back to the middle tier
Here's where it gets a little bit tricky, client side. The ADO Recordset keeps track of all the changes the user made to the original data we received. In order for us to update the database, we need to send these changes back by invoking the server side method UpdateCustomers. The problem we have now is that the XML stream generated client side will contain both the changes and the original data. This is extremely not efficient since the only thing the server needs are the updates. Take a look at the code associated to the button bUpdateCustomers:
procedure TForm1.bUpdateCustomersClick(Sender: TObject);
var
xml, errs: widestring;
begin
xml := FilterUpdates(ADODataSet1.Recordset as _Recordset);
[..]
if not fCustomersDataObject.UpdateCustomers(xml, errs) then
MessageDlg(errs, mtError, [mbOK], 0)
else
begin
ShowMessage('Updated!');
ADODataSet1.Recordset.CancelUpdate;
end;
end;
I am generating XML using the function FilterUpdates. Here's the code:
function FilterUpdates(const aRecordset: _Recordset): string;
var
DOMDoc: IXMLDOMDocument2;
RemNode, DataNode: IXMLDOMNode;
i, offset: integer;
begin
DOMDoc := CoDOMDocument30.Create;
DOMDoc.Async := FALSE;
aRecordset.Save(DOMDoc, adPersistXML);
DataNode := DOMDoc.selectSingleNode('xml/rs:data');
offset := 0;
for i := 0 to (DataNode.childNodes.length - 1) do
begin
if (DataNode.childNodes[i - offset].nodeName = 'z:row') then
begin
RemNode := DataNode.removeChild(DataNode.childNodes[i - offset]);
offset := offset + 1;
end;
end;
result := DOMDoc.xml
end;
This function was inspired by an article I found on the MSDN website. Take a look at it by clicking here. What I do here is nothing but filtering out all the original data while keeping the updates. This is how the XML stream will look after this operation:
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
<s:Schema id="RowsetSchema">
<s:ElementType name="row" content="eltOnly" rs:updatable="true">
<s:AttributeType name="CustomerID" rs:number="1" rs:writeunknown="true" rs:basecatalog="Northwind" rs:basetable="CUSTOMERS" rs:basecolumn="CustomerID" rs:keycolumn="true">
<s:datatype dt:type="string" dt:maxLength="5" rs:fixedlength="true" rs:maybenull="false"/>
</s:AttributeType>
[..] <s:extends type="rs:rowbase"/>
</s:ElementType>
</s:Schema>
<rs:data>
<rs:update>
<rs:original>
<z:row CustomerID="WOLZA" CompanyName="Wolski Zajazd" ContactName="Zbyszek Piestrzeniewicz" ContactTitle="Owner" Address="ul. Filtrowa 68" City="Warszawa" PostalCode="01-012" Country="Poland" Phone="(26) 642-7012" Fax="(26) 642-7012"/>
</rs:original>
<z:row CompanyName="Wolski Zajazdxxx"/>
</rs:update>
<rs:insert>
<z:row CustomerID="A " CompanyName="Test"/>
<z:row CustomerID="B " CompanyName="Test #2"/>
</rs:insert>
</rs:data>
</xml>
The final step: updating the database
We are almost done. Now we sent the batch of updates back to the server using XML and we invoked the method UpdateCustomers. If you take a look at the code, you will find the following:
function TCustomersDataObject.UpdateCustomers(const someChanges: WideString; out
Errors: WideString): WordBool;
begin
result := FALSE;
Errors := '';
try
// Updates the database
UpdateSource(DBConnStr, someChanges);
result := TRUE;
except
on E: Exception do
Errors := E.Message;
end;
end;
There's nothing particoular in that code except the call to the method UpdateSource listed below:
procedure UpdateSource(const aConnection: OleVariant; const someChanges: string);
var
rs, conn: OleVariant;
begin
rs := XMLToRecordset(someChanges);
case VarType(aConnection) of
varUnknown,
varDispatch: conn := IUnknown(aConnection) as _Connection;
else
begin
conn := CoConnection.Create;
conn.Open(aConnection, '', '', 0);
end;
end;
rs.ActiveConnection := conn;
rs.Filter := adFilterPendingRecords;
rs.UpdateBatch(adAffectAllChapters);
rs.Close;
end;
We just converted the delta of changes into a Recordset and we finally used its UpdateBatch method to persist the changes to the database.
Conclusion
You can find more information about ADO on the Microsoft website. What you saw above should give you a pretty good idea of how updates work in a distributed, stateless environment and how you can use existing technologies to do this efficiently and in a very simple way.
2005. április 18., hétfő
Show/Hide the "Start" button
Problem/Question/Abstract:
Show/Hide the "Start" button
Answer:
Solve 1:
The following procedure hides or shows the start button:
procedure hideStartbutton(visi: boolean);
var
Tray,Child: HWnd;
c: array[0..127] of Char;
s: string;
begin { hideStartButton }
Tray := FindWindow('Shell_TrayWnd', nil);
Child := GetWindow(Tray, GW_CHILD);
while Child <> 0 do
begin
if GetClassName(Child, c, SizeOf(c)) > 0 then
begin
s := StrPas(c);
if UpperCase(s) = 'BUTTON' then
begin
// IsWindowVisible(Child)
startbutton_handle := Child;
if visi then
ShowWindow(Child, 1)
else
ShowWindow(Child, 0)
end
end;
Child := GetWindow(Child, GW_HWNDNEXT)
end
end; { hideStartButton }
Solve 2:
To show:
EnableWindow(FindWindowEx(FindWindow('Shell_TrayWnd', nil), 0, 'Button', nil), TRUE);
To hide:
EnableWindow(FindWindowEx(FindWindow('Shell_TrayWnd', nil), 0, 'Button', nil), FALSE);
2005. április 17., vasárnap
Viewing Targa Bitmap File Format in Delphi (256-colors)
Problem/Question/Abstract:
How to view Bitmap in Targa File Format (*.tga) using Delphi ?
Answer:
This is quite simple way to answer above question: viewing Targa file format using Delphi (not compress and limited only 256 colors).
Here is the example code:
const
FERRORMSG2 = 'Sorry, Unsupported Compressed(RLE) File Format';
FERRORMSG3 = 'Sorry, Unsupported More Than 256 Colours File Format';
type
TArrBuff = array[1..512] of Byte;
TPalette_Cell = record
b2, g2, r2: byte;
end;
TPal = array[0..255] of TPalette_Cell;
TPPal = ^TPal;
TTGA_Header = record // Targa(TGA) HEADER //
IDLength, ColorMap, ImageType: byte;
ClrMapSpes: array[1..5] of byte;
XAwal, YAwal, Width, Height: SmallInt;
BpPixel, ImageDescription: byte;
end;
var
pal: TPPal;
pFile: file;
buffer: TArrBuff;
FTgaHeader: TTGA_Header;
procedure THPTGA.ReadImageData2Bitmap;
var
i, j, idx: integer;
begin
Seek(pFile, sizeof(FtgaHeader) + FtgaHeader.IDLength + 768);
for i := FtgaHeader.Height - 1 downto FtgaHeader.YAwal do
begin
BlockRead(pFile, buffer, FtgaHeader.Width);
for j := FtgaHeader.XAwal to FtgaHeader.Width - 1 do
begin
idx := j - FtgaHeader.XAwal + 1;
SetPixel(Bitmap.Canvas.Handle, j, i, rgb(pal^[buffer[idx]].r2,
pal^[buffer[idx]].g2, pal^[buffer[idx]].b2));
end;
end;
end;
procedure THPTGA.LoadFromFile(const FileName: string);
begin
AssignFile(pFile, FileName);
{$I-}Reset(pFile, 1);
{$I+}
if (IOResult = 0) then
begin
try
BlockRead(pFile, FtgaHeader, SizeOf(FtgaHeader));
// checking unsupported features here
if (FtgaHeader.ImageType > 3) then
begin
MessageBox(Application.Handle, FERRORMSG2, 'TGA Viewer Error', MB_ICONHAND);
exit;
end;
if (FtgaHeader.BpPixel > 8) then
begin
MessageBox(Application.Handle, FERRORMSG3, 'TGA Viewer Error', MB_ICONHAND);
exit;
end;
GetMem(pal, 768);
try
Bitmap.Width := FtgaHeader.Width;
Bitmap.Height := FtgaHeader.Height;
// if use Color-Map and Uncompressed then read it
if (FtgaHeader.ImageType = 1) then
BlockRead(pFile, pal^, 768);
ReadImageData2Bitmap;
finally
FreeMem(pal);
end;
finally
CloseFile(pFile);
end;
end
else
MessageBox(Application.Handle, 'Error Opening File', 'TGA Viewer Error',
MB_ICONHAND);
end;
How to try this code ?? Just call the "LoadFromFile" procedure above in your application (probably with little modification offcourse, especially about the name of mainForm that I used here [THPTGA]).
Hopefully It can help you.
For full source code and simple application that use this, you can look and download from my website: www.geocities.com/h4ryp/delphi.html.
Note: At http://www.delphi-gems.com/Graphics.php you can download the freeware Delphi unit GraphicEx.pas which makes a bunch of graphics formats available to Delphi programs, among them PCX, TIFF, TGA, etc. The formats are embedded into Delphi in the same way as Borland's jpeg unit.
2005. április 16., szombat
Auxiliary TQuery used with queries built at run time
Problem/Question/Abstract:
Auxiliary queries built at run time make you copy-paste a lot, replicating your code. Why not keep it to a minimum, making it easy to read and mantain ?
Answer:
Do you have an auxiliary TQuery on your form that you use to build dynamic queries, like
'SELECT Count(id) FROM Clients'
and a bit latter you use the same TQuery to
'SELECT Count(Phone_numbers) FROM Clients WHERE area = '1''
and latter on you use it again to
'SELECT Count(area) FROM Contacts'
and so on...
If you have an auxiliar TQuery to run all these queries, you probably have a lot of similar code replicated in your application to load the query string, prepare the query, run the query and finally close the query.
Since replication is not a good thing when it comes to maintenance, why not abstract the queries from the code so that you just have to pass the TQuery object, the query string and, optionally, the database, if you use different databases.
Here's sometinh I've been using for a while that creates that abstraction layer:
procedure Execute(Q: TQuery; S: string; DBName = '');
begin
with Q do
begin
if DBName <> '' then
DatabaseName := DBName;
try
Close;
finally
SQL.Clear;
end;
SQL.Add(S);
try
Prepare;
while not (Prepared) do
;
Open;
finally
;
end;
end;
end;
Using this procedure, you can reduce the amount of code and maintenance effort to a minimum, since you can prepare and open the queries just by using:
Execute(MyTQuery, 'SELECT Count(id) FROM Clients');
Execute(MyTQuery, 'SELECT Count(Phone_numbers) FROM Clients WHERE area = ' 1 '');
Execute(MyTQuery, 'SELECT Count(area) FROM Contacts', 'Contacts_database');
Execute(MyTQuery, 'SELECT Count(ZIP) FROM Zip_Codes', 'Address_database');
Execute(MyTQuery, 'SELECT Names FROM Vip_Clients', 'clients_database');
After calling the Execute procedure, you are able to read the result from the TQuery as usual.
This procedure simplifies the process of checking if the object is opened, close it if necessary, prepare the query for execution, release the resources to other processes while not ready and finally run the query.
2005. április 15., péntek
Have a menu in any form
Problem/Question/Abstract:
I need to display a menu in the expert's "main" form. Now this form was created with TForm1.Create() and therefore not an application's main window.
Answer:
Although the menu is displayed in design mode properly, at runtime it will not appear. Even manually assigning the menu with Menu := MainMenu will not help.
Use the API function SetMenu in the FormCreate handler as shown below:
procedure TForm1.FormCreate(Sender: TObject);
var
h: integer;
begin
h := ClientHeight;
SetMenu(Handle, MainMenu1.Handle);
ClientHeight := h;
end;
2005. április 14., csütörtök
Convert a RFC time string into TDateTime and VV
Problem/Question/Abstract:
How to convert a RFC time string into TDateTime and vv.
Answer:
Conversion of date & time formats: RFC <-> TDateTime (local & UNC) RFC: e.g. 03.01.2001 05:45:00 -0500
TDateTime -> RFC use
DateTimeToRfcTime(date, diff, gmt)
RFC -> TDateTime use
RfcTimeToDateTime(time, gmt)
function DateTimeToRfcTime(
dt: TDateTime;
iDiff: integer;
blnGMT: boolean = false): string;
{*
Explanation:
iDiff is the local offset to GMT in minutes
if blnGMT then Result is UNC time else local time
e.g. local time zone: ET = GMT - 5hr = -300 minutes
dt is TDateTime of 3 Jan 2001 5:45am
blnGMT = true -> Result = 'Wed, 03 Jan 2001 05:45:00 GMT'
blnGMT = false -> Result = 'Wed, 03 Jan 2001 05:45:00 -0500'
*}
const
Weekday: array[1..7] of string =
('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
Month: array[1..12] of string = (
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
var
iDummy: Word;
iYear: Word;
iMonth: Word;
iDay: Word;
iHour: Word;
iMinute: Word;
iSecond: Word;
strZone: string;
begin
if blnGMT then
begin
dt := dt - iDiff / 1440;
strZone := 'GMT';
end
else
begin
iDiff := (iDiff div 60) * 100 + (iDiff mod 60);
if iDiff < 0 then
strZone := Format('-%.4d', [-iDiff])
else
strZone := Format('+%.4d', [iDiff]);
end;
DecodeDate(dt, iYear, iMonth, iDay);
DecodeTime(dt, iHour, iMinute, iSecond, iDummy);
Result := Format('%s, %.2d %s %4d %.2d:%.2d:%.2d %s', [
Weekday[DayOfWeek(dt)], iDay, Month[iMonth], iYear,
iHour, iMinute, iSecond, strZone]);
end;
function RfcTimeToDateTime(
strTime: string;
blnGMT: boolean = true): TDateTime;
{*
Explanation:
if blnGMT then Result is UNC time else local time
e.g. local time zone: ET = GMT - 5hr = -0500
strTime = 'Wed, 03 Jan 2001 05:45:00 -0500'
blnGMT = true -> FormatDateTime('...', Result) = '03.01.2001 10:45:00'
blnGMT = false -> FormatDateTime('...', Result) = '03.01.2001 05:45:00'
*}
const
wd = 'sun#mon#tue#wed#thu#fri#sat';
month = 'janfebmaraprmayjunjulaugsepoctnovdec';
var
s: string;
dd: Word;
mm: Word;
yy: Word;
hh: Word;
nn: Word;
ss: Word;
begin
s := LowerCase(Copy(strTime, 1, 3));
if Pos(s, wd) > 0 then
Delete(strTime, 1, Pos(' ', strTime));
s := Trim(Copy(strTime, 1, Pos(' ', strTime)));
Delete(strTime, 1, Length(s) + 1);
dd := StrToIntDef(s, 0);
s := LowerCase(Copy(strTime, 1, 3));
Delete(strTime, 1, 4);
mm := (Pos(s, month) div 3) + 1;
s := Copy(strTime, 1, 4);
Delete(strTime, 1, 5);
yy := StrToIntDef(s, 0);
Result := EncodeDate(yy, mm, dd);
s := strTime[1] + strTime[2];
hh := StrToIntDef(strTime[1] + strTime[2], 0);
nn := StrToIntDef(strTime[4] + strTime[5], 0);
ss := StrToIntDef(strTime[7] + strTime[8], 0);
Delete(strTime, 1, 9);
Result := Result + EncodeTime(hh, nn, ss, 0);
if (CompareText(strTime, 'gmt') <> 0) and blnGMT then
begin
hh := StrToIntDef(strTime[2] + strTime[3], 0);
nn := StrToIntDef(strTime[4] + strTime[5], 0);
if strTime[1] = '+' then
Result := Result - EncodeTime(hh, nn, 0, 0)
else
Result := Result + EncodeTime(hh, nn, 0, 0);
end;
end;
2005. április 13., szerda
Determine the default browser
Problem/Question/Abstract:
This article describes how to determine the default browser and its version
Answer:
The WinAPI provides an excellent function for this purpose: FindExecutable. This function returns the application associated with the given file. The application associated with a .htm file is the default browser.
The prototype is
function FindExecutable(FileName, Directory: PChar; Result: PChar): HINST;
Unfortunately this function needs an existent file. For this reason it is necessary to create a temporary one.
Basic function
The basic function performs the following steps:
Determine the temp directory
Create a file with ".htm" extension.
Use FindExecutable
Delete the temporary file
type
TBrowserInformation = record
Name: string;
Path: string;
Version: string;
end;
function GetDefaultBrowser: TBrowserInformation;
var
tmp: PChar;
res: PChar;
begin
tmp := StrAlloc(255);
res := StrAlloc(255);
try
GetTempPath(255, tmp);
FileCreate(tmp + 'htmpl.htm');
FindExecutable('htmpl.htm', tmp, Res);
Result.Name := ExtractFileName(res);
Result.Path := ExtractFilePath(res);
SysUtils.DeleteFile(tmp + 'htmpl.htm');
finally
StrDispose(tmp);
StrDispose(res);
end;
end;
Long File Name
Now, if you run that function you will notice that there is a small inconvenience: The function returns the location of the default browser as short path. The next function will try to convert it to the long format.
function LongPathName(ShortPathName: string): string;
var
PIDL: PItemIDList;
Desktop: IShellFolder;
WidePathName: WideString;
AnsiPathName: AnsiString;
begin
Result := ShortPathName;
if Succeeded(SHGetDesktopFolder(Desktop)) then
begin
WidePathName := ShortPathName;
if Succeeded(Desktop.ParseDisplayName(0, nil, PWideChar(WidePathName), ULONG(nil^), PIDL, ULONG(nil^))) then
try
SetLength(AnsiPathName, MAX_PATH);
SHGetPathFromIDList(PIDL, PChar(AnsiPathName));
Result := PChar(AnsiPathName);
finally CoTaskMemFree(PIDL);
end;
end;
end;
Version Information
The next step is to extend the basic function with error handling and the ability to get the default browsers version.
function GetDefaultBrowser: TBrowserInformation;
var
tmp: PChar;
res: PChar;
Version: Pointer;
VersionInformation: Pointer;
VersionInformationSize: Integer;
Dummy: Integer;
begin
tmp := StrAlloc(255);
res := StrAlloc(255);
Version := nil;
try
GetTempPath(255, tmp);
if FileCreate(tmp + 'htmpl.htm') <> -1 then
begin
if FindExecutable('htmpl.htm', tmp, res) > 32 then
begin
Result.Name := ExtractFileName(res);
Result.Path := LongPathName(ExtractFilePath(res));
// Try to determine the Browser Version
VersionInformationSize := GetFileVersionInfoSize(Res, Dummy);
if VersionInformationSize > 0 then
begin
GetMem(VersionInformation, VersionInformationSize);
GetFileVersionInfo(Res, 0, VersionInformationSize, VersionInformation);
VerQueryValue(VersionInformation, ('StringFileInfo040904E4ProductVersion'),
Pointer(Version), Dummy);
if Version <> nil then
Result.Version := PChar(Version);
FreeMem(VersionInformation);
end;
end
else
raise EGetDefaultBrowser.Create('Can''t determine the executable.');
SysUtils.DeleteFile(tmp + 'htmpl.htm');
end
else
raise EGetDefaultBrowser.Create('Can''t create temporary file.');
finally
StrDispose(tmp);
StrDispose(res);
end;
end;
2005. április 12., kedd
How to highlight a TBitmap with a color overlay
Problem/Question/Abstract:
Does anyone know of a way that I can achieve the same effect on a bitmap that Windows achieves when you single click on an icon on the desktop? In other words, I want to "highlight" a bitmap and let the user know that it is selected.
Answer:
To me it appears as if the icons on my desktop are highlighted by overlaying them with a certain color, so I guess the following routine is of use.
procedure Highlight(aSource, ATarget: TBitmap; AColor: TColor);
{Alters ASource to ATarget by making it appear as if looked through colored glass as given by AColor.
ASource, ATarget must have been created. Isn't as slow as it looks. Physics courtesy of a post by K.H. Brenner}
var
i, j: Integer;
s, t: pRGBTriple;
r, g, b: byte;
cl: TColor;
begin
cl := ColorToRGB(AColor);
r := GetRValue(cl);
g := GetGValue(cl);
b := GetBValue(cl);
aSource.PixelFormat := pf24bit;
ATarget.PixelFormat := pf24bit;
ATarget.Width := aSource.Width;
ATarget.Height := aSource.Height;
for i := 0 to aSource.Height - 1 do
begin
s := ASource.Scanline[i];
t := ATarget.Scanline[i];
for j := 0 to aSource.Width - 1 do
begin
t^.rgbtBlue := (b * s^.rgbtBlue) div 255;
t^.rgbtGreen := (g * s^.rgbtGreen) div 255;
t^.rgbtRed := (r * s^.rgbtRed) div 255;
inc(s);
inc(t);
end;
end;
end;
2005. április 11., hétfő
Getting a lot of files in one stream
Problem/Question/Abstract:
How to get a lot of files in one stream
And get it back to :)
Answer:
Some times i want to Have multiple files in a stream couse then i dont have to send a lot of files but just one .
So heres a little code sniped to get it in and out again.
procedure TForm1.ThisISHowIPutFilesIn;
var
ABigFileStream, SomeSmallFiles: TMemoryStream;
begin
ABigFileStream := TMemoryStream.Create;
try
SomeSmallFiles := TMemoryStream.Create;
try
SomeSmallFiles.LoadFromFile('C:\SomeSmalFile1.txt');
AddToStream(SomeSmallFiles, ABigFileStream);
SomeSmallFiles.LoadFromFile('C:\SomeSmalFile2.txt');
AddToStream(SomeSmallFiles, ABigFileStream);
// enz
finally
SomeSmallFiles.Free;
end;
ABigFileStream.SaveToFile('C:\MrBig.DDD')
finally
ABigFileStream.free;
end;
end;
procedure TForm1.ThisISHowIGetStufOut;
var
ABigFileStream, SomeSmallFiles: TMemoryStream;
begin
ABigFileStream := TMemoryStream.Create;
try
ABigFileStream.LoadFromFile('C:\MrBig.DDD');
SomeSmallFiles := TMemoryStream.Create;
try
GetFromStream(ABigFileStream, SomeSmallFiles, 0);
SomeSmallFiles.SaveToFile('C:\SomeSmalFile1.txt');
GetFromStream(ABigFileStream, SomeSmallFiles, 1);
SomeSmallFiles.SaveToFile('C:\SomeSmalFile2.txt');
// enz
finally
SomeSmallFiles.Free;
end;
finally
ABigFileStream.free;
end;
end;
procedure TForm1.AddToStream(Source, Dest: TStream);
var
Size: Integer;
begin
Source.position := 0;
// Keep the size by puting it in the first byte
Size := Source.Size;
Dest.Write(Size, SizeOf(Integer));
Dest.CopyFrom(Source, Source.size);
end;
procedure TForm1.GetFromStream(Source, Dest: TStream; Index: Integer);
var
Size, I: Integer;
begin
Source.Position := 0;
for i := 0 to index - 1 do
begin
Source.Read(Size, SizeOf(Integer));
Source.Position := Source.Position + Size;
end;
// if where all the way up the file pointer then someting went wrong :(
if Source.position = Source.Size then
raise EAccessViolation.Create('Index Out Of Bounds');
// Get the desired file size
Source.Read(Size, SizeOf(Integer));
// Clear Dest Buffer
Dest.Position := 0;
Dest.Size := 0;
Dest.CopyFrom(Source, Size);
end;
2005. április 10., vasárnap
Get Computer MAC Address
Problem/Question/Abstract:
How can you get the computers MAC address?
Answer:
Solve 1:
The following code will allow you to retrieve the MAC address of your computer. It is a close translation of the C++ code found at the Borland Community at:
http://community.borland.com/article/0,1410,26040,00.html
You must include the NB30 unit in your uses clause for this code to work.
Simply call the GetMACAddress routine for the address of the first network adapter installed.
uses
NB30;
function GetAdapterInfo(Lana: Char): string;
var
Adapter: TAdapterStatus;
NCB: TNCB;
begin
FillChar(NCB, SizeOf(NCB), 0);
NCB.ncb_command := Char(NCBRESET);
NCB.ncb_lana_num := Lana;
if Netbios(@NCB) <> Char(NRC_GOODRET) then
begin
Result := 'mac not found';
Exit;
end;
FillChar(NCB, SizeOf(NCB), 0);
NCB.ncb_command := Char(NCBASTAT);
NCB.ncb_lana_num := Lana;
NCB.ncb_callname := '*';
FillChar(Adapter, SizeOf(Adapter), 0);
NCB.ncb_buffer := @Adapter;
NCB.ncb_length := SizeOf(Adapter);
if Netbios(@NCB) <> Char(NRC_GOODRET) then
begin
Result := 'mac not found';
Exit;
end;
Result :=
IntToHex(Byte(Adapter.adapter_address[0]), 2) + '-' +
IntToHex(Byte(Adapter.adapter_address[1]), 2) + '-' +
IntToHex(Byte(Adapter.adapter_address[2]), 2) + '-' +
IntToHex(Byte(Adapter.adapter_address[3]), 2) + '-' +
IntToHex(Byte(Adapter.adapter_address[4]), 2) + '-' +
IntToHex(Byte(Adapter.adapter_address[5]), 2);
end;
function GetMACAddress: string;
var
AdapterList: TLanaEnum;
NCB: TNCB;
begin
FillChar(NCB, SizeOf(NCB), 0);
NCB.ncb_command := Char(NCBENUM);
NCB.ncb_buffer := @AdapterList;
NCB.ncb_length := SizeOf(AdapterList);
Netbios(@NCB);
if Byte(AdapterList.length) > 0 then
Result := GetAdapterInfo(AdapterList.lana[0])
else
Result := 'mac not found';
end;
Solve 2:
function TUserInfo.GetPrimaryNicMacAddress(): string;
// Here is a function that we use to get the MAC address.
// It comes from the CoCreateGUID API call. In W2K
// Microsoft changed the underlying call in CoCreateGUID
// to a random value instead of the MAC address that is
// why the function checks which version of Windows is
// running and then makes the appropriate API call.
type
TGUID = record
A, B: word;
D, M, S: word;
MAC: array[1..6] of byte;
end;
var
UuidCreateFunc: function(var guid: TGUID): HResult; stdcall;
handle: THandle;
g: TGUID;
WinVer: _OSVersionInfoA;
i: integer;
ErrCode: HResult;
begin
WinVer.dwOSVersionInfoSize := sizeof(WinVer);
getversionex(WinVer);
handle := LoadLibrary('RPCRT4.DLL');
if WinVer.dwMajorVersion >= 5 then {Windows 2000 }
@UuidCreateFunc := GetProcAddress(Handle, 'UuidCreateSequential')
else
@UuidCreateFunc := GetProcAddress(Handle, 'UuidCreate');
UuidCreateFunc(g);
result := '';
for i := 1 to 6 do
result := result + IntToHex(g.MAC[i], 2);
end;
2005. április 9., szombat
How to get a list of all table names in a database
Problem/Question/Abstract:
How does one get a list of the table names in a database? The TDatabase.Datasets list seems to be only open tables. I need to get at the unopened ones as well.
Answer:
procedure TForm1.Button1Click(Sender: TObject);
var
ts: TStringlist;
begin
ts := TStringlist.create;
Session.GetTableNames('DBDEMOS', '*.DB', false, false, ts);
listbox1.items.assign(ts);
ts.free;
end;
2005. április 8., péntek
Updating a table with data from another table with Local SQL
Problem/Question/Abstract:
The UPDATE sentence of Local SQL (the SQL used by the BDE) doesn't support JOIN... How two update a table with data from another table?
Answer:
Orders.db
Customer.db
CustNo
ShipToAddr1
ShipToAddr2
<<--------->
CustNo
Addr1
Addr2
Assuming that we wanted to update the fields ShipToAddr1 and ShipToAddr2 of the Orders.db table with the values of the fields Addr1 and Addr2 respectively from the table Customer.db, for those records of Orders that have both fields blank, and joining the tables by the field CustNo present in both tables, perhaps we would be temped to write:
UPDATE Orders INNER JOIN Customer
ON Customer.CustNo = Orders.CustNo
SET ShipToAddr1 = Addr1, ShipToAddr2 = Addr2
WHERE ShipToAddr1 = "" AND ShipToAddr2 = ""
However, in Local SQL (the one used by the BDE), joins are not supported in the UPDATE statement, and we have to use subqueries to achieve the expected result:
UPDATE Orders
SET ShipToAddr1 = (SELECT Addr1 FROM Customer WHERE
Customer.CustNo = Orders.CustNo),
ShipToAddr2 = (SELECT Addr2 FROM customer WHERE
Customer.CustNo = Orders.CustNo)
WHERE ShipToAddr1 = "" AND ShipToAddr2 = ""
In the "UPDATE statement" topic of the Local SQL Guide you can find an example of a 1-to-many relationship that uses grouping the subqueries.
Copyright (c) 2001 Ernesto De Spirito
Visit: http://www.latiumsoftware.com/delphi-newsletter.php
2005. április 7., csütörtök
Basic authentication in ISAPI/CGI applications
Problem/Question/Abstract:
How to password protect ISAPI/CGI actions with basic authentication?
Answer:
It is very easy to protect a web server virtual directory with basic authentication.
Supose you have an ISAPI application with 3 actions and you want to password protect only one of them. This example shows you how you could do that with only one ISAPI application.
Source Code:
- This 2 lines tells browser to prompt for user name and password:
Response.StatusCode := 401; // Promp for user name and password
Response.WWWAuthenticate := 'Basic realm="Delphi"'; // Title
- Browser sends user name and password and we can get it:
Request.Authorization
- But information is encoded with Base64. There a lot of free source code that implements Base64 encode/decode. The following line returns decoded data in mAuthorization.
FBase64.DecodeData(Copy(Request.Authorization, 6, Length(Request.Authorization)),
mAuthorization);
Component Download: authen.zip
2005. április 6., szerda
How to animate a window while opening a form
Problem/Question/Abstract:
How to animate a window while opening a form
Answer:
This project uses two forms:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
procedure FormClick(Sender: TObject);
private
procedure FocusAnimation(DC: HDC; AnimRect: TRect; Steps, Speed, Direction: Integer);
public
end;
const
FA_IN = 0;
FA_OUT = 1;
var
Form1: TForm1;
implementation
uses Unit2;
{$R *.DFM}
procedure TForm1.FormClick(Sender: TObject);
var
WRect: TRect;
begin
GetWindowRect(Form2.handle, WRect);
FocusAnimation(GetDC(0), WRect, 20, 10, FA_OUT);
{Open form2}
Form2.ShowModal;
end;
procedure TForm1.FocusAnimation(DC: HDC; AnimRect: TRect; Steps, Speed, Direction: Integer);
var
cv, animx, animy, animwidth, animheight: Integer;
xp, yp: Double;
FRect: TRect;
cancel: Boolean;
begin
{Steps = number of steps during open/close operation,
Speed = time between the steps,
Direction = inner/outer direction}
animx := AnimRect.left + (AnimRect.right - AnimRect.left) div 2;
animy := AnimRect.top + (AnimRect.bottom - AnimRect.top) div 2;
animwidth := AnimRect.right - AnimRect.left;
animheight := AnimRect.bottom - AnimRect.top;
xp := animwidth div 2 / Steps; {horizontal}
yp := animheight div 2 / Steps; {vertical}
if Direction = FA_OUT then
cv := 0
else
cv := Steps;
while not cancel do
begin
FRect := Rect(Round(animx - cv * xp), Round(animy - cv * yp),
Round(animx + cv * xp), Round(animy + cv * yp));
DrawFocusRect(DC, FRect);
Sleep(Speed);
DrawFocusRect(DC, FRect);
if Direction = FA_OUT then
begin
Inc(cv);
if cv > Steps then
cancel := True;
end
else
begin
Dec(cv);
if cv < 0 then
cancel := True;
end;
end;
end;
end.
2005. április 5., kedd
Creating threads straight from the WinAPI
Problem/Question/Abstract:
How can I implement threads in my programs without using the VCL TThread object?
Answer:
I've done extensive work in multi-threaded applications. And in my experience, there have been times when a particular program I'm writing should be written as a multi-threaded application, but using the TThread object just seems like overkill. For instance, I write a lot of single function programs; that is, the entire functionality (beside the user interface portion) of the program is contained in one single execution procedure or function. Usually, this procedure contains a looping mechanism (e.g. FOR, WHILE, REPEAT) that operates on a table or an incredibly large text file (for me, that's on the order of 500MB-plus!). Since it's just a single procedure, using a TThread is just too much work for my preferences.
For those experienced Delphi programmers, you know what happens to the user interface when you run a procedure with a loop in it: The application stops receiving messages. The most simple way of dealing with this situation is to make a call to Application.ProcessMessages within the body of the loop so that the application can still receive messages from external sources. And in many, if not most, cases, this is a perfectly valid thing to do. However, if some or perhaps even one of the steps within the loop take more than a couple of seconds to complete processing — as in the case of a query — Application.ProcessMessages is practically useless because the application will only receive messages at the time the call is made. So what you ultimately achieve is intermittent response at best. Using a thread, on the other hand, frees up the interface because the process is running completely separate from the main thread of the program where the interface resides. So regardless of what you execute within a loop that is running in a separate thread, your interface will never get locked up.
Don't confuse the discussion above with multi-threaded user interfaces. What I'm talking about is executing long background threads that won't lock up your user interface while they run. This is an important distinction to make because it's not really recommended to write multi-user interfaces, because each thread that is created in the system has its own message queue. Thus, a message loop must be created to fetch messages out of the queue so they can be dispatched appropriately. The TApplication object that controls the UI would be the natural place to set up message loops for background threads, but it's not set up to detect when other threads are executed. The gist of all this is that the sole reason you create threads is to distribute processing of independent tasks. Since the UI and controls are fairly integrated, threads just don't make sense here because in order to make the separate threads work together, you have to synchronize them to work in tandem, which practically defeats threading altogether!
I mentioned above that the TThread object is overkill for really simple threaded stuff. This is strictly an opinion, but experience has made me lean that way. In any case, what is the alternative to TThread in Delphi?
The solution isn't so much an alternative as it is going a bit more low-level into the Windows API. I've said this several times before: The VCL is essentially one giant wrapper around the Windows API and all its complexities. But fortunately for us, Delphi provides a very easy way to access lower-level functionality beyond the wrapper interface with which it comes. And even more fortunate for us, we can create threads using a simple Windows API function called CreateThread to bypass the TThread object altogether. As you'll see below, creating threads in this fashion is incredibly easy to do.
Setting Yourself Up
There are two distinct steps for creating a thread: 1)Create the thread itself, then 2) Provide a function that will act as the thread entry point. The thread function or thread entry point is the function (actually the address of the function) that tells your thread where to start.
Unlike a regular function, there are some specific requirements regarding the thread function that you have to obey:
You can give the function any name you want, but it must be a function name (ie. function MyThreadFunc)
The function must have a single formal parameter of type Pointer (I'll discuss this below)
The function return type is always LongInt
Its declaration must always be preceded by the stdcall directive. This tells the compiler that the function will be passing parameters in the standard Windows convention.
Whew! That seems like a lot but it's really not as complicated as it might seem from the description above. Here's an example declaration:
function MyThreadFunc(Ptr: Pointer): LongInt; stdcall;
That's it! Hope I didn't get you worried. The CreateThread call is a bit more involved, but it too is not very complicated once you understand how to call it. Here's its declaration, straight out of the help file:
function CreateThread
(lpThreadAttributes: Pointer; //Address of thread security attributes
dwStackSize: DWORD; //Thread stack size
lpStartAddress: TFNThreadStartRoutine; //Address of the thread function
lpParameter: Pointer; //Input parameter for the thread
dwCreationFlags: DWORD; //Creation flags
var lpThreadId: DWORD): //ThreadID reference
THandle; stdcall; //Function returns a handle to the thread
This is not as complicated as it seems. First of all, you rarely have to set security attributes, so that can be set to nil. Secondly, in most cases, your stack size can be 0 (actually, I've never found an instance where I have to set this to a value higher than zero). You can optionally pass a parameter through the lpParameter argument as a pointer to a structure or address of a variable, but I've usually opted to use global variables instead (I know, this breaking a cardinal rule of structured programming, but it sure eases things). Lastly, I've rarely had to set creation flags unless I want my thread to start in a suspended state so I can do some preprocessing. For the most part, I set this value as zero.
Now that I've thoroughly confused you, let's look at an example function that creates a thread:
procedure TForm1.Button1Click(Sender: TObject);
var
thr: THandle;
thrID: DWORD;
begin
FldName := ListBox1.Items[ListBox1.ItemIndex];
thr := CreateThread(nil, 0, @CreateRecID, nil, 0, thrID);
if (thr = 0) then
ShowMessage('Thread not created');
end;
Embarrassingly simple, right? It is. To make the thread in the function above, I declared two variables, thr and thrID, which stand for the handle of the thread and its identifier, respectively. I set a global variable that the thread function will access immediately before the call to CreateThread, then make the declaration, assigning the return value of the function to thr and inputting the address of my thread function, and the thread ID variable. The rest of the parameters I set to nil or 0. Not much to it.
Notice that the procedure that actually makes the call is an OnClick handler for a button on a form. You can pretty much create a thread anywhere in your code as long as you set up properly. Here's the entire unit code for my program; you can use it for a template. This program is actually fairly simple. It adds an incremental numeric key value to a table called RecID, based on the record number (which makes things really easy). Browse the code; we'll discuss it below:
unit main;
interface
uses
Windows, Messages, SysUtils, Classes,
Graphics, Controls, Forms, Dialogs, DB, DBTables, StdCtrls, ComCtrls,
Buttons;
type
TForm1 = class(TForm)
Edit1: TEdit;
Label1: TLabel;
OpenDialog1: TOpenDialog;
SpeedButton1: TSpeedButton;
Label2: TLabel;
StatusBar1: TStatusBar;
Button1: TButton;
ListBox1: TListBox;
procedure SpeedButton1Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
TblName: string;
FldName: string;
implementation
{$R *.DFM}
function CreateRecID(P: Pointer): LongInt; stdcall;
var
tbl: TTable;
I: Integer;
ses: TSession;
msg: string;
begin
Randomize; //Initialize random number generator
I := 0;
{Disable the Execute button so another thread can't be executed
while this one is running}
EnableWindow(Form1.Button1.Handle, False);
{If you're going to access any data in a thread, you have to create a
separate }
ses := TSession.Create(Application);
ses.SessionName := 'MyRHSRecIDSession' + IntToStr(Random(1000));
tbl := TTable.Create(Application);
with tbl do
begin
Active := False;
SessionName := ses.SessionName;
DatabaseName := ExtractFilePath(TblName); //TblName is a global variable set
TableName := ExtractFileName(TblName); //in the SpeedButton's OnClick handler
Open;
First;
try
{Start looping structure}
while not EOF do
begin
if (State <> dsEdit) then
Edit;
msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
{Display message in status bar}
SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
FieldByName(FldName).AsInteger := RecNo;
Next;
end;
finally
Free;
ses.Free;
EnableWindow(Form1.Button1.Handle, True);
end;
end;
msg := 'Operation Complete!';
SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
end;
procedure TForm1.SpeedButton1Click(Sender: TObject);
var
tbl: TTable;
I: Integer;
begin
with OpenDialog1 do
if Execute then
begin
Edit1.Text := FileName;
TblName := FileName;
tbl := TTable.Create(Application);
with tbl do
begin
Active := False;
DatabaseName := ExtractFilePath(TblName);
TableName := ExtractFileName(TblName);
Open;
LockWindowUpdate(Self.Handle);
for I := 0 to FieldCount - 1 do
begin
ListBox1.Items.Add(Fields[I].FieldName);
end;
LockWindowUpdate(0);
Free;
end;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
thr: THandle;
thrID: DWORD;
begin
FldName := ListBox1.Items[ListBox1.ItemIndex];
thr := CreateThread(nil, 0, @CreateRecID, nil, 0, thrID);
if (thr = 0) then
ShowMessage('Thread not created');
end;
end.
The most important function here, obviously, is the thread function, CreateRecID. Let's take a look at it:
function CreateRecID(P: Pointer): LongInt; stdcall;
var
tbl: TTable;
I: Integer;
ses: TSession;
msg: string;
begin
Randomize; //Initialize random number generator
I := 0;
{Disable the Execute button so another thread can't be executed
while this one is running}
EnableWindow(Form1.Button1.Handle, False);
{If you're going to access any data in a thread, you have to create a
separate }
ses := TSession.Create(Application);
ses.SessionName := 'MyRHSRecIDSession' + IntToStr(Random(1000));
tbl := TTable.Create(Application);
with tbl do
begin
Active := False;
SessionName := ses.SessionName;
DatabaseName := ExtractFilePath(TblName); //TblName is a global variable set
TableName := ExtractFileName(TblName); //in the SpeedButton's OnClick handler
Open;
First;
try
{Start looping structure}
while not EOF do
begin
if (State <> dsEdit) then
Edit;
msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
{Display message in status bar}
SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
FieldByName(FldName).AsInteger := RecNo;
Next;
end;
finally
Free;
ses.Free;
EnableWindow(Form1.Button1.Handle, True);
end;
end;
msg := 'Operation Complete!';
SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
end;
This is a pretty basic function. I'll leave it up to you to follow the flow of execution. However, let's look at some very interesting things that are happening in the thread function.
First of all, notice that I created a TSession object before I created the table I was going to access. This is to ensure that the program will behave itself with the BDE. This is required any time you access a table or other data source from within the context of a thread. I've explained this in more detail in another article called How Can I Run Queries in Threads? Directly above that, I made a call to the Windows API function EnableWindow to disable the button that executes the code. I had to do this because since the VCL is not thread-safe, there's no guarantee I'd be able to successfully access the button's Enabled property safely. So I had to disable it using the Windows API call that performs enabling and disabling of controls.
Moving on, notice how I update the caption of a status bar that's on the bottom of the my form. First, I set the value of a text variable to the message I want displayed:
msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
Then I do a SendMessage, sending the WM_SETTEXT message to the status bar:
SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
SendMessage will send a message directly to a control and bypass the window procedure of the form that owns it.
Why did I go to all this trouble? For the very same reason that I used EnableWindow for the button that creates the thread. But unfortunately, unlike the single call to EnableWindow, there's no other way to set the text of a control other than sending it the WM_SETTEXT message.
The point to all this sneaking behind the VCL is that for the most part, it's not safe to access VCL properties or procedures in threads. In fact, the objects that are particularly dangerous to access from threads are those descended from TComponent. These comprise a large part of the VCL, so in cases where you have to perform some interaction with them from a thread, you'll have to use a roundabout method. But as you can see from the code above, it's not all that difficult.
Of the thousands of functions in the Windows API, CreateThread is one of the most simple and straightforward. I spent a lot of time explaining things here, but there's a lot of ground I didn't cover. Use this example as a template for your thread exploration. Once you get the hang of it, you'll use threads in practically everything you do.
Feliratkozás:
Bejegyzések (Atom)