2009. május 31., vasárnap
Shift, Ctrl, Alt Key checking
Problem/Question/Abstract:
How to detect if Ctrl, Alt or Shift key is pressed
Answer:
function CtrlDown: Boolean;
var
State: TKeyboardState;
begin
GetKeyboardState(State);
Result := ((State[vk_Control] and 128) <> 0);
end;
function ShiftDown: Boolean;
var
State: TKeyboardState;
begin
GetKeyboardState(State);
Result := ((State[vk_Shift] and 128) <> 0);
end;
function AltDown: Boolean;
var
State: TKeyboardState;
begin
GetKeyboardState(State);
Result := ((State[vk_Menu] and 128) <> 0);
end;
The following example demonstrates checking if the Shift key is pressed during a Button Click.
procedure TForm1.Button1Click(Sender: TObject);
begin
if ShiftDown then
Form1.Caption := 'Shift'
else
Form1.Caption := 'No Shift';
end;
2009. május 30., szombat
Validating email addresses in Delphi
Problem/Question/Abstract:
Is an email address valid?
Answer:
Nowadays it's very common that our programs store email addresses in databases as part of the data of personnel, customers, providers, etc. When prompting the user for an email address, how do we know if the entered value is formally correct? In this article I'll show you how to validate email addresses using a variation of the RFC #822.
The RFC #822 rules the "STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES".
According to this rule, the following are valid email addresses:
John Doe johndoe@server.com
John Doe
"John Doe" johndoe@server.com
"John Doe"
The purpose of my code is not to validate such things, but strictly what is necessary to reach a single recipient (like "johndoe@server.com"), that in the specification is referred as an "addr-spec", which has the form:
local-part@domain
local-part = one "word" or more, separated by periods
domain = one "sub-domain" or more, separated by periods
A "word" can be an "atom" or a "quoted-string":
atom = one or more chars in the range #33..#126 except ()<>@,;:\/".[]
quoted-string = A text enclosed in double quotes that can contain 0 or
more characters (#0..#127) except '"' and #13. A backslash ('\')
quotes the next character.
A "sub-domain" can be a "domain-ref" (an "atom") or a "domain-literal":
domain-literal = A text enclosed in brackets that can contain 0 or
more characters (#0..#127) except '[', ']' and #13. A backslash ('\')
quotes the next character.
According to the RFC 822, extended characters (#128..#255) cannot be part of an email address, however many mail servers accept them and people use them, so I'm going to take them into account.
The RFC 822 is very open about domain names. For a real Internet email address maybe we should restrict the domain part. You can read more about domain names in the RFC #1034 and RFC #1035.
For the RFC 1034 and the RFC 1035, a domain name is formed by "sub-domains" separated by periods, and each subdomain starts with a letter ('a'..'z', 'A'..'Z') and should be followed by zero or more letters, digits and hyphens, but cannot end with a hyphen. We are going to consider that a valid domain should have at least two "sub-domains" (like "host.com").
Now that we have the rules clear, let's get to the work. The algorithm for the function resembles a states-transition machine. Characters of the string are processed in a loop, and for each character first we determine in which state the machine is and then we process the character accordingly, to determine if the machine should continue in that state, switch to a different state or produce an error (breaking the loop). These kind of algorithms are extensively treated in programming-algorithms textbooks, so let's get right to the code:
function ValidEmail(email: string): boolean;
// Returns True if the email address is valid
// Author: Ernesto D'Spirito
const
// Valid characters in an "atom"
atom_chars = [#33..#255] - ['(', ')', '<', '>', '@', ',', ';', ':',
'\', '/', '"', '.', '[', ']', #127];
// Valid characters in a "quoted-string"
quoted_string_chars = [#0..#255] - ['"', #13, '\'];
// Valid characters in a subdomain
letters = ['A'..'Z', 'a'..'z'];
letters_digits = ['0'..'9', 'A'..'Z', 'a'..'z'];
subdomain_chars = ['-', '0'..'9', 'A'..'Z', 'a'..'z'];
type
States = (STATE_BEGIN, STATE_ATOM, STATE_QTEXT, STATE_QCHAR,
STATE_QUOTE, STATE_LOCAL_PERIOD, STATE_EXPECTING_SUBDOMAIN,
STATE_SUBDOMAIN, STATE_HYPHEN);
var
State: States;
i, n, subdomains: integer;
c: char;
begin
State := STATE_BEGIN;
n := Length(email);
i := 1;
subdomains := 1;
while (i <= n) do
begin
c := email[i];
case State of
STATE_BEGIN:
if c in atom_chars then
State := STATE_ATOM
else if c = '"' then
State := STATE_QTEXT
else
break;
STATE_ATOM:
if c = '@' then
State := STATE_EXPECTING_SUBDOMAIN
else if c = '.' then
State := STATE_LOCAL_PERIOD
else if not (c in atom_chars) then
break;
STATE_QTEXT:
if c = '\' then
State := STATE_QCHAR
else if c = '"' then
State := STATE_QUOTE
else if not (c in quoted_string_chars) then
break;
STATE_QCHAR:
State := STATE_QTEXT;
STATE_QUOTE:
if c = '@' then
State := STATE_EXPECTING_SUBDOMAIN
else if c = '.' then
State := STATE_LOCAL_PERIOD
else
break;
STATE_LOCAL_PERIOD:
if c in atom_chars then
State := STATE_ATOM
else if c = '"' then
State := STATE_QTEXT
else
break;
STATE_EXPECTING_SUBDOMAIN:
if c in letters then
State := STATE_SUBDOMAIN
else
break;
STATE_SUBDOMAIN:
if c = '.' then
begin
inc(subdomains);
State := STATE_EXPECTING_SUBDOMAIN
end
else if c = '-' then
State := STATE_HYPHEN
else if not (c in letters_digits) then
break;
STATE_HYPHEN:
if c in letters_digits then
State := STATE_SUBDOMAIN
else if c <> '-' then
break;
end;
inc(i);
end;
if i <= n then
Result := False
else
Result := (State = STATE_SUBDOMAIN) and (subdomains >= 2);
end;
Any collaboration to improve this function will be welcome.
Copyright (c) 2001 Ernesto De Spirito
Visit: http://www.latiumsoftware.com/delphi-newsletter.php
2009. május 29., péntek
How to copy RTF text to a TMemo via a TStringStream
Problem/Question/Abstract:
How to copy RTF text to a TMemo via a TStringStream
Answer:
Here is a method to copy the formatted text to a memo via a stringstream. The same approach should work to store it in a database field that takes a string.
procedure TForm1.Button1Click(Sender: TObject);
var
ss: TStringStream;
begin
ss := TStringStream.Create(EmptyStr);
try
richedit1.plaintext := false;
richedit1.lines.savetostream(ss);
memo1.text := ss.DataString;
finally
ss.free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
ss: TStringStream;
begin
ss := TStringStream.Create(memo1.text);
try
richedit1.plaintext := false;
richedit1.lines.LoadFromstream(ss);
finally
ss.free;
end;
end;
To simply copy formatted text from a TRichEdit to a string, you could also use:
function GetRawRTFText(aRichedit: TRichedit): string;
var
SS: TStringstream;
begin
SS := TStringstream.Create(EmptyStr);
try
aRichedit.plaintext := False;
arichedit.Lines.SaveToStream(SS);
Result := SS.DataString;
finally
SS.Free
end;
end;
2009. május 28., csütörtök
How to check if Netscape or IExplorer is running and get the current URL
Problem/Question/Abstract:
How can I find out if Netscape is running or not? And if it is running, how can I get the current URL address that is displaying? The same with IE.
Answer:
Here's an example which gets you both the URL and the title of the page into one string. For explorer replace 'Netscape' with 'IExplorer':
uses
DdeMan;
procedure TForm1.Button1Click(Sender: TObject);
var
DDE: TDdeClientConv;
begin
DDE := TDdeClientConv.Create(self);
if DDE.SetLink('Netscape', 'WWW_GetWindowInfo') then
Edit1.Text := DDE.RequestData('0xFFFFFFFF, sURL, sTitle')
else
ShowMessage('Netscape is not running');
DDE.Free;
end;
2009. május 27., szerda
How to split a wave file
Problem/Question/Abstract:
I have a very big wave file and I want to split it into two separately files. But, this two files have to stand alone. You should can play and edit them like another wave files.
Answer:
If you know how wave files work, this is very easy.
First you need the complete file header of the old wave file:
type
TWaveHeader = record
ident1: array[0..3] of Char; // Must be "RIFF"
len: DWORD; // remaining length after this header
ident2: array[0..3] of Char; // Must be "WAVE"
ident3: array[0..3] of Char; // Must be "fmt "
reserv: DWORD; // Reserved Size
wFormatTag: Word; // format type
nChannels: Word; // number of channels (i.e. mono, stereo, etc.)
nSamplesPerSec: DWORD; //sample rate
nAvgBytesPerSec: DWORD; //for buffer estimation
nBlockAlign: Word; //block size of data
wBitsPerSample: Word; //number of bits per sample of mono data
cbSize: Word; //the count in bytes of the size of
ident4: array[0..3] of Char; //Must be "data"
end;
You can load the file header with this function:
function GetWaveHeader(FileName: TFilename): TWaveHeader;
const
riff = 'RIFF';
wave = 'WAVE';
var
f: TFileStream;
w: TWaveHeader;
begin
if not FileExists(Filename) then
exit;
try
f := TFileStream.create(Filename, fmOpenRead);
f.Read(w, Sizeof(w));
if w.ident1 <> riff then
begin
Showmessage('This is not a RIFF File');
exit;
end;
if w.ident2 <> wave then
begin
Showmessage('This is not a valid wave file');
exit;
end;
finally
f.free;
end;
Result := w;
end;
Now we have all for creating the code for spliting the wave file:
function SplitWave(Source, Dest1, Dest2: TFileName; Pos: Integer): Boolean;
var
f1, f2, f3: TfileStream;
w: TWaveHeader;
p: Integer;
begin
Result:=False
if not FileExists(Source) then
exit;
try
w := GetWaveHeader(Source);
p := Pos - Sizeof(TWaveHeader);
f1 := TFileStream.create(Source, fmOpenRead);
f2 := TFileStream.create(Dest1, fmCreate);
f3 := TFileStream.create(Dest2, fmCreate);
{++++++++++Create file 1 ++++++++++++++++}
w.len := p;
f2.Write(w, Sizeof(w));
f1.position := Sizeof(w);
f2.CopyFrom(f1, p);
{++++++++++++++++++++++++++++++++++++++++}
{+++++++++++Create file 2 +++++++++++++++}
w.len := f1.size - Pos;
f3.write(w, Sizeof(w));
f1.position := Pos;
f3.CopyFrom(f1, f1.size - pos);
{++++++++++++++++++++++++++++++++++++++++}
finally
f1.free;
f2.free;
f3.free;
end;
Result:=True;
end;
2009. május 26., kedd
How to change the font color of all components on a form at runtime
Problem/Question/Abstract:
I would like to change the font color on all components on a form at runtime (and the components owned by the components etc). I devised a recursive algorithm using RTTI that accepts a TComponent as a parameter. It works to some extent, but I still have to use 'if' statements to cast the object to a particular descendant, resulting in about 30 lines of code to test for all of the components I use. Also, some objects (TColumnTitle), are not descended from TComponent, even though they have a font property.
Answer:
This may do the trick (with D6 and maybe D5):
uses
TypInfo;
{ ... }
var
i: integer;
aFont: TFont;
begin
for i := 0 to aComponent.ComponentCount - 1 do
begin
aFont := TFont(GetOrdProp(aComponent.Components[i], 'Font'));
if assigned(aFont) then
aFont.Color := clWhite;
end;
end;
With D4:
{ ... }
var
i: integer;
aFont: TFont;
pi: PPropInfo;
begin
for i := 0 to aComponent.ComponentCount - 1 do
begin
pi := GetPropInfo(aComponent.Components[i].ClassInfo, 'Font');
if assigned(pi) then
TFont(GetOrdProp(aComponent.Components[i], pi)).Color := clWhite;
end;
end;
2009. május 25., hétfő
How do I create a file association for my win32 application
Problem/Question/Abstract:
How do I create a file association for my win32 application (Update for 98/ME/NT5(2000)/ME) ?
Answer:
In Win32, create a new registry entry under the HKEY_CLASSES_ROOT root key that points to the file extension, the command line to invoke, and the icon to display.
Update:
Windows will execute '\shell\open\command' from the KEY pointed in (Default) value.
So you can :
Clear Default value of extention key with itself ex: .jpg -> .jpg. This make windows use '\shell\open\command' from the proper KEY. (As shown in example)
Create enother key like "MyProgExt" with '\shell\open\command' and point any extension you need to it. This way the extension key default value will be: MyProgExt.
Example:
uses Registry,
procedure TForm1.FileFormatAssociations;
var
reg: TRegistry;
FileExt: string;
begin
reg := TRegistry.Create;
reg.RootKey := HKEY_CLASSES_ROOT;
reg.LazyWrite := false;
FileExt := '.jpg';
//Clear Key - This is important !!!
reg.OpenKey(FileExt, true);
reg.WriteString('', FileExt);
reg.CloseKey;
//Invoke the program passing the file name as the first parameter
reg.OpenKey(FileExt + '\shell\open\command', true);
reg.WriteString('', Application.ExeName + ' "%1"');
reg.CloseKey;
//Use the first icon in the executable to display
reg.OpenKey(FileExt + '\DefaultIcon', true);
reg.WriteString('', Application.ExeName + ',0');
reg.CloseKey;
reg.free;
end;
2009. május 24., vasárnap
How to make caption blink?
Problem/Question/Abstract:
How to make caption blink?
Answer:
If you want to have your form's caption bar blink, you may think of using a timer and owner-drawing the non-client area :-) But there is a much easier way:
Use the API function
FlashWindow(h: hWnd; bInvert: boolean);
2009. május 23., szombat
How to sort directory entries by date / time
Problem/Question/Abstract:
How to sort directory entries by date / time
Answer:
type
PEntryItem = ^TEntryItem;
TEntryItem = record
FName: string;
FDate: TDateTime;
FSize: Integer;
end;
function SortList(item1, item2: Pointer): integer;
var
i1: PEntryItem absolute Item1;
i2: PEntryItem absolute Item2;
begin
if (i1^.fdate > i2^.fdate) then
result := 1 {greater}
else if (i1^.fdate < i2^.fdate) then
result := -1 {smaller}
else
result := 0; {equals}
end;
procedure DoTheJob;
var
List: TList;
i: Integer;
procedure GetDirEntries(Path: string);
var
Dta: TSearchRec;
P: PEntryItem;
begin
if (Path <> '') and (Path[Length(Path)] = '\') then
setlength(Path, Pred(Length(Path)));
if FindFirst(Path + '\*.*', faAnyFile, Dta) <> 0 then
exit; {Nothing}
repeat
{remove next line if you want these two as well}
if (Dta.Name = '.') or (dta.name = '..') then
continue;
New(P);
P^.FName := Dta.Name;
P^.FSize := Dta.Size;
P^.FDate := FileDateToDateTime(Dta.Time);
List.Add(P);
until
(FindNext(Dta) <> 0);
FindClose(Dta);
end;
begin
List := TList.Create;
List.Clear;
try
GetDirEntries('C:\Windows');
if (list.Count > 0) then
List.Sort(SortList);
{Now you have a list with all the files in the directory
sorted by its date/time field. To access them do as follows:}
for i := 0 to list.count - 1 do
begin
with PEntryItem(List.Items[i])^ do
begin
{I assume you have a Form called MainFrm with a Listbox1}
MainFrm.ListBox1.Items.Add(Format('%s %d %s', [FName, FSize,
FormatDateTime('dd/mm/yyyy HH:NN:SS', FDate)]));
end;
end;
finally
{make sure to relase the memory allocated}
while (list.count > 0) do
begin
Dispose(PEntryItem(List.Items[0]));
List.Delete(0);
end;
List.Free;
end;
end;
2009. május 22., péntek
How to specify a message subject for an email
Problem/Question/Abstract:
When I click on a button, I would like to have a message prepared with some information (just a few words). How can I specify this information in the message part or in the subject part of the email?
Answer:
uses
ShellApi;
procedure TForm1.Button1Click(Sender: TObject);
var
mail: string;
begin
mail := 'mailto:you@you.com' + '?subject=hello' + '&cc=me@me.com' + '&body=Delphi is cool! ;)';
ShellExecute(Self.Handle, 'open', PChar(mail), nil, nil, SW_SHOWNORMAL);
end;
2009. május 21., csütörtök
How to detect a mouse movement over a word in a TRichEdit
Problem/Question/Abstract:
How to detect a mouse movement over a word in a TRichEdit
Answer:
Solve 1:
How can I detect when the mouse runs over a word or an expression in a TRichEdit?
You start by adding a handler to the TRichEdit's OnMouseMove event. In the handler you get to the character under the mouse with:
uses
richedit; {for EM_EXLINEFROMCHAR}
var
pt: TPoint;
charindex, lineindex, charoffset: Integer;
begin
pt := Point(X, Y);
charindex := richedit.perform(Messages.EM_CHARFROMPOS, 0, integer(@pt));
if charindex >= 0 then
begin
lineindex := richedit.perform(EM_EXLINEFROMCHAR, 0, charindex);
charoffset := charindex - richedit.perform(EM_LINEINDEX, lineindex, 0);
end;
end;
Lineindex and charoffset now allow you to get the line out of the richedit.Lines array and look at the characters value. Note that you have to use charoffset+1 as index into the string, e.g.:
S := richedit.lines[lineindex];
charundercursor := S[charoffset + 1];
If charundercursor is a letter (IsCharAlpha( charundercursor )) you could now walk backwards in S to find the start of the word and forward to find its end. Similar for an "expression" (whatever that may mean in your context).
Solve 2:
Using the source code above, how can I determine, whether the mouse is over a phrase like 'Inprise Delphi', which has the text attribute Bold and the colour Lime?
Well, you have to jump through some hoops, I'm afraid. The TRichEdit component has a method FindText that can be used to search for text, ignoring any text attributes. It would go something like this:
{additional vars required}
S: string;
foundPos, oldSelStart: Integer;
{after determining charindex as above}
S := 'Inprise Delphi'; {text to search for}
foundpos := richedit.FindText(S, charindex - Length(S) + 1, charindex + Length(S),
[stWholeWord, stMatchCase]);
if foundpos > = 0 then
begin
{found the text, check attributes}
{for this we need to select the text}
oldSelStart := richedit.SelStart;
richedit.SelStart := foundPos;
richedit.SelLength := Length(S);
with richedit.SelAttributes do
if ((ConsistentAttributes * [caBold, caColor]) = [caBold, caColor]) and
(Color = clLime) and (fsBold in Style) then
{we have a match!}
else
SelStart := oldSelStart;
end;
Solve 3:
How can I tell from a WM_MOUSEMOVE message that the mouse cursor is over a particular character or line of a TRichedit? I am simulating a very simplistic browser and want to know when the user is over a "hyperlink" which is really just a TRichedit with some of the text underlined. That way I can tell the cursor to change from crHand and back.
uses
richedit;
var
pt: TPoint;
charindex, row, col: Integer;
{ ... }
GetCursorPos(pt);
pt := richedit.screentoclient(pt);
charindex := richedit1.perform(Messages.EM_CHARFROMPOS, 0, integer(@pt));
Row := richedit1.PerForm(EM_EXLINEFROMCHAR, 0, charindex);
Col := charindex - richedit1.Perform(EM_LINEINDEX, Row, 0);
Solve 4:
Looking for a way to find the character position the mouse is over during a TRichEdit.onMouseMove event.
var
msg: TMessage;
pt: TPoint;
pos: integer; {offset into RichEdit.Text}
{ ... }
pt.x := X;
pt.y := Y;
msg.Result := SendMessage(RichEdit.handle, EM_CHarFromPos, 0, Integer(@pt));
pos := msg.ResultLo;
if RichEdit.Text[pos] = '?' then
{ ... }
2009. május 20., szerda
How to return a variant type property value based on the text name of the property
Problem/Question/Abstract:
I am looking for code that returns a property value (presumably as a variant type) based on the text name of the property, similar to the TTable.FieldByName function.
Answer:
This should get you started. You need to study the comments "typinfo.pas" for more info.
unit MorePropInfo;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
TFrmMorePropInfo = class(TForm)
Button1: TButton;
Button2: TButton;
ListBox1: TListBox;
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
FrmMorePropInfo: TFrmMorePropInfo;
implementation
{$R *.DFM}
uses
TypInfo;
procedure GetPropertyValues(AObj: TObject; AValues: TStrings);
var
count: integer;
data: PTypeData;
default: string;
i: integer;
info: PTypeInfo;
propList: PPropList;
propInfo: PPropInfo;
propName: string;
value: variant;
begin
info := AObj.ClassInfo;
data := GetTypeData(info);
GetMem(propList, data^.PropCount * SizeOf(PPropInfo));
try
count := GetPropList(info, tkAny, propList);
for i := 0 to count - 1 do
begin
propName := propList^[i]^.Name;
propInfo := GetPropInfo(info, propName);
if propInfo <> nil then
begin
case propInfo^.PropType^.Kind of
tkClass, tkMethod:
value := '$' + IntToHex(GetOrdProp(AObj, propInfo), 8);
tkFloat:
value := GetFloatProp(AObj, propInfo);
tkInteger:
value := GetOrdProp(AObj, propInfo);
tkString, tkLString, tkWString:
value := GetStrProp(AObj, propInfo);
tkEnumeration:
value := GetEnumProp(AObj, propInfo);
else
value := '???';
end;
if propInfo.default = longint($80000000) then
default := 'none'
else
default := IntToStr(propInfo.default);
AValues.Add(Format('%s: %s [default: %s]', [propName, value, default]));
{$80000000 apparently indicates "no default"}
end;
end;
finally
FreeMem(propList, data^.PropCount * SizeOf(PPropInfo));
end;
end;
procedure TFrmMorePropInfo.Button2Click(Sender: TObject);
var
count: integer;
data: PTypeData;
i: integer;
info: PTypeInfo;
propList: PPropList;
propInfo: PPropInfo;
propName: string;
propVal: variant;
tmpS: string;
begin
info := Button2.ClassInfo;
data := GetTypeData(info);
GetMem(propList, data^.PropCount * SizeOf(PPropInfo));
try
count := GetPropList(info, tkAny, propList);
ListBox1.Clear;
for i := 0 to count - 1 do
begin
propName := propList^[i]^.Name;
propInfo := GetPropInfo(info, propName);
if propInfo <> nil then
begin
case propInfo^.PropType^.Kind of
tkClass, tkMethod:
propVal := '$' + IntToHex(GetOrdProp(Button2, propInfo), 8);
tkFloat:
propVal := GetFloatProp(Button2, propInfo);
tkInteger:
propVal := GetOrdProp(Button2, propInfo);
tkString, tkLString, tkWString:
propVal := GetStrProp(Button2, propInfo);
tkEnumeration:
propVal := GetEnumProp(Button2, propInfo);
else
propVal := '...';
end;
tmpS := propVal;
ListBox1.Items.Add(Format('%s: %s [default: %s]', [propName, tmpS, '$'
+ IntToHex(propInfo.default, 8)]));
{$80000000 apparently indicates "no default"}
end;
end;
finally
FreeMem(propList, data^.PropCount * SizeOf(PPropInfo));
end;
end;
end.
2009. május 19., kedd
How to close another application (2)
Problem/Question/Abstract:
I'm using the command CreateProcess(nil, PChar('PKUNZIP ..."),nil,nil,false,0,nil,nil,SI,PI) to execute PKUNZIP inside my program. But after the execution of PKUNZIP the window remains opened. How do I detect if PKUNZIP finishes its execution and how do I close the window after that?
Answer:
Closing it gently :
{ ... }
var
Handle: THandle;
begin
Handle := FindWindow(classname, windowname); {Look this one up in the help file ...}
if Handle <> 0 then
if MessageBox(Handle,
PChar('Do you really want me to try to kill this application ?'),
'Please Confirm', MB_YESNO) = mrYES then
begin
PostMessage(Handle, WM_QUIT, 0, 0);
end;
end;
To close it with more brute force:
procedure TggProcessViewer.KillProcess(hProcess: THandle);
var
PH: THandle;
lpExitCode: DWord;
begin
PH := OpenProcess(PROCESS_TERMINATE or PROCESS_QUERY_INFORMATION, false, hProcess);
if PH <> 0 then
begin
if GetExitCodeProcess(PH, lpExitCode) then
begin
if MessageBox(Handle,
PChar('Do you really want me to try to kill this process ?'),
'Please Confirm', MB_YESNO) = mrYES then
begin
TerminateProcess(PH, lpExitCode);
MessageBox(Handle, PChar('should be dead now...'), PChar('Check it out...'),
MB_OK);
end;
end
else
MessageBox(Handle, PChar('Could not retreive the ExitCode for this process.' +
#13 + #13 + SysErrorMessage(GetLastError)),
PChar('Something went wrong...'), MB_OK);
CloseHandle(PH);
end
else
MessageBox(Handle, PChar('Could not get access to this process.' + #13 + #13
+ SysErrorMessage(GetLastError)), PChar('Something went wrong...'),
MB_OK);
Refresh;
end;
2009. május 18., hétfő
Determine if ColCount has changed in a TStringGrid
Problem/Question/Abstract:
I'm writing a TStringGrid descendant in which I would like to know, if the ColCount is being changed, because I have some other objects hidden in the component, that should be updated, when the ColCount changes. How can I accomplish that?
Answer:
You can try the following:
unit MyStringGrid;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Grids;
type
TMyStringGrid = class(TStringGrid)
private
FColCount: Integer;
FOnColCountChanged: TNotifyEvent;
procedure SetColCount(Value: Integer);
protected
procedure ColCountChanged; virtual;
public
constructor Create(aOwner: TComponent); override;
published
property ColCount: Integer read FColCount write SetColCount default 5;
property OnColCountChanged: TNotifyEvent read FOnColCountChanged
write FOnColCountChanged;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TMyStringGrid]);
end;
constructor TMyStringGrid.Create(aOwner: TComponent);
begin
inherited Create(aOwner);
FColCount := 5;
end;
procedure TMyStringGrid.SetColCount(Value: Integer);
begin
if FColCount <> Value then
begin
FColCount := Value;
inherited ColCount := FColCount;
ColCountChanged;
end;
end;
procedure TMyStringGrid.ColCountChanged;
begin
{do dependend stuff here}
if Assigned(FOnColCountChanged) then
FOnColCountChanged(Self);
end;
end.
2009. május 17., vasárnap
Retrieve icons from the system image list
Problem/Question/Abstract:
Having the system image list in a TImageList, how can I get the index for special icons like floppy disk, hard drive, removable drive, etc., which do not represent file types? I can see that these icons are actually present (for example, 11 = CD-ROM, 8 = Hard-Drive on my machine ...). But I would prefer not to use hard-coded constants.
Answer:
uses
ShellAPI;
var
sfi: TSHFileInfo;
begin
SHGetFileInfo("DUMMY.ABC", FILE_ATTRIBUTE_NORMAL, sfi, sizeof(sfi),
SHGFI_ICON or SHGFI_USEFILEATTRIBUTES);
Index := sfi.iIcon;
end;
By using the second parameter you can get icons for file system items that do not physically exist in the first parameter, which is not completely obvious in the help for SHGetFileInfo. For example.
SHGetFileInfo(" * .DOC", FILE_ATTRIBUTE_NORMAL, sfi, sizeof(sfi),
SHGFI_ICON or SHGFI_USEFILEATTRIBUTES);
gets the icon associated with DOC files. To get drives use
SHGetFileInfo("A: \", FILE_ATTRIBUTE_NORMAL, sfi, sizeof(sfi),
SHGFI_ICON or SHGFI_USEFILEATTRIBUTES);
etc.
Note that this retrieves the LARGE icon. See the help on getting small icon with other flags.
2009. május 16., szombat
Convert a UNIX linefeed delimited text file to a DOS CR/LF delimited file
Problem/Question/Abstract:
I'm having an issue converting a Unix 1.15 GB text file to a Windows file. It always seems my application runs out of memory during the conversion. Does anyone have any ideas as to how to accomplish this?
Answer:
It sounds like you're trying to read the whole file into memory or something. How does this work:
program unix2dos;
{$APPTYPE CONSOLE}
uses
SysUtils;
var
fp1: file;
fp2: TextFile;
buffer, buf2: array[1..8192] of Char;
numread: Integer;
i: Integer;
begin
if paramcount <> 2 then
begin
writeln('USAGE : UNIX2DOS <input file> <output file>');
writeln(' Takes UNIX text file (linefeed delimited) and');
writeln(' converts it to a DOS (CR/LF) delimited text file.');
halt(10);
end;
if FileExists(Paramstr(1)) then
begin
AssignFile(fp1, paramstr(1));
Reset(fp1, 1);
AssignFile(fp2, paramstr(2));
SetTextBuf(fp2, buf2);
rewrite(fp2);
repeat
BlockRead(fp1, buffer, sizeof(buffer), Numread);
if Numread <> 0 then
begin
for i := 1 to Numread do
begin
if buffer[i] = #10 then
writeln(fp2)
else
write(fp2, buffer[i]);
end;
end;
until
NumRead = 0;
close(fp1);
close(fp2);
end
else
writeln('Could not find file : ', paramstr(1));
end.
2009. május 15., péntek
How to copy data from a TMemoryStream to a string without using an array of char as buffer
Problem/Question/Abstract:
How to copy data from a TMemoryStream to a string without using an array of char as buffer
Answer:
var
S: string;
begin
SetLength(S, MemStream.Size);
MemStream.Read(S[1], MemStream.Size);
end;
2009. május 14., csütörtök
Search and replace
Problem/Question/Abstract:
Search and replace
Answer:
Almost every text editor / word processor has the ability to search for a given string and replace it with another. If you're planning on adding similar functionality to your application, here's an example of where all of it can start:
function SearchAndReplace(
sSrc, sLookFor, sReplaceWith
: string)
: string;
var
nPos, nLenLookFor: integer;
begin
nPos := Pos(sLookFor, sSrc);
nLenLookFor := Length(sLookFor);
while (nPos > 0) do
begin
Delete(sSrc, nPos, nLenLookFor);
Insert(sReplaceWith, sSrc, nPos);
nPos := Pos(sLookFor, sSrc);
end;
Result := sSrc;
end;
For example, let's say you have a string -- 'this,is,a,test' -- and you want to replace the commas with spaces. Here's how you'd call SearchAndReplace():
SearchAndReplace('this,is,a,test', ',', ' ')
SearchAndReplace() will now return the string 'this is a test'.
2009. május 13., szerda
How to specify a DefaultExpression for a TField object
Problem/Question/Abstract:
How to specify a DefaultExpression for a TField object
Answer:
If you set up a field attribute set and associate that set with a field in your table that will work. If you set the value in the object inspector it will let you enter in a string but it will not reflect the value at runtime. If you try to set the TField.DefaultExpression property at runtime like this:
MyField.DefaultExpression := 'MyValue'; // (Wrong)
It will compile but you will not get a default value at runtime when you create a new record in the table by, say, clicking on the + on the DBNavigator. To get the default value to take at runtime the code assignment needs to be:
MyField.DefaultExpression := '''MyValue'''; // (Correct)
In the Object Inspector you just need to put "MyValue". Use single quotes in the Object Inspector.
Note: The default value support for the BDE dataset's is implemented in BDE itself and is not currently supported in the ADOExpress components. If you want to set defaults in your application using ADO I suggest writing a OnNewRecord event.
2009. május 12., kedd
Converting To and From Hexadecimal Numbers
Problem/Question/Abstract:
I have seen quite a few articles on how to convert a decimal number to hexa-decimal, yet none have used the most obviuos function. Delphi offers the solution already!
Answer:
The solution is quite simple.
If you have an Integer intX and you want to convert it into an hexadecimal number use IntToHex(intNumber, intDigits)
Expample 1:
strHex = IntToHex(intX, 2);
Delphi will create a hexadecimal string representing intX with at least two digits. Therefore if intX is 10, strHex will Hold 0A.
To convert back to decimal you can use either StrToInt or StrToIntDef. However, you must ensure that the string contains a leading dollar-sign ($).
Example 2.1 (Completting this article using Example 1):
intX = StrToInt('$' + strHex);
Delphi will take strHex (0A), you may have to provide the leading $ and will return the Decimal Value 10.
Example 2.2 (Completting this article using Example 1):
intX = StrToIntDef('$' + strHex, -1);
Delphi will take strHex (0A), you may have to provide the leading $ and will return the Decimal Value 10. If the function fails, it will return the pre-defined value -1.
2009. május 11., hétfő
How to check if a string contains accented characters
Problem/Question/Abstract:
Our compiler does not support characters with accents. What is the best way to check if the strings we pass to the compiler have accents or not?
Answer:
If you check to see that every character in the string falls in the range of #$00 to #$7F, you can be sure that it contains no accented letters. Something like this will do:
function ContainsAccents(const S: string): Boolean;
var
I: Integer;
begin
for I := 1 to Length(S) do
if S[I] > #$7F then
begin
Result := True;
Exit;
end;
Result := False;
end;
2009. május 10., vasárnap
How to create isometric maps
Problem/Question/Abstract:
I'm planning to make an isometric map based game. Now, to do this, I need to know if the user clicked on one (or more) squares, for example, a building or a creature. I cannot figure out how to do this.
Answer:
Create a new project. On the form, create a TImage and align it to client. Also assign the form's OnCreate event, and the Image's OnMouseUp and OnMouseDown events. Paste this code into Unit1 and run. A 10x10 grid will be drawn. Click in it to highlight a square.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls;
type
TForm1 = class(TForm)
Image1: TImage;
procedure FormCreate(Sender: TObject);
procedure Image1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure Image1MouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure Image1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
private
public
end;
var
Form1: TForm1;
implementation
uses
Math;
{$R *.DFM}
var
XC: Integer;
YC: Integer;
LastX: Single;
LastY: Single;
const
Scale = 20;
procedure Map(const WorldX: Single; const WorldY: Single; out DisplayX: Integer;
out DisplayY: Integer);
begin
DisplayX := Round(XC + Scale * (WorldX - WorldY) * 0.5 * Sqrt(3));
DisplayY := Round(YC + Scale * (WorldX + WorldY) * 0.5);
end;
procedure UnMap(const DisplayX: Integer; const DisplayY: Integer; out WorldX: Single;
out WorldY: Single);
var
Sum: Single;
Diff: Single;
begin
Diff := (DisplayX - XC) / (0.5 * Scale * Sqrt(3));
Sum := (DisplayY - YC) / (0.5 * Scale);
WorldY := (Sum - Diff) / 2;
WorldX := Sum - WorldY;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
X1: Integer;
Y1: Integer;
X2: Integer;
Y2: Integer;
begin
XC := ClientWidth div 2;
YC := ClientHeight div 2;
with Image1.Picture.Bitmap do
begin
Width := Image1.Width;
Height := Image1.Height;
end;
for I := -5 to 5 do
begin
Map(I, 5, X1, Y1);
Map(I, -5, X2, Y2);
with Image1.Picture.Bitmap.Canvas do
begin
MoveTo(X1, Y1);
LineTo(X2, Y2);
end;
Map(5, I, X1, Y1);
Map(-5, I, X2, Y2);
with Image1.Picture.Bitmap.Canvas do
begin
MoveTo(X1, Y1);
LineTo(X2, Y2);
end;
end;
end;
procedure ColorizeCell(const Color: TColor);
var
PolygonData: array[0..3] of TPoint;
begin
if ((Abs(LastX) < 5) and (Abs(LastY) < 5)) then
begin
Map(Floor(LastX), Floor(LastY), PolygonData[0].X, PolygonData[0].Y);
Map(Floor(LastX), Ceil(LastY), PolygonData[1].X, PolygonData[1].Y);
Map(Ceil(LastX), Ceil(LastY), PolygonData[2].X, PolygonData[2].Y);
Map(Ceil(LastX), Floor(LastY), PolygonData[3].X, PolygonData[3].Y);
with Form1.Image1.Picture.Bitmap.Canvas do
begin
Brush.Style := bsSolid;
Brush.Color := Color;
Polygon(PolygonData);
end;
end;
end;
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
Unmap(X, Y, LastX, LastY);
ColorizeCell(clRed);
end;
procedure TForm1.Image1MouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
ColorizeCell(clWhite);
end;
procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
ColorizeCell(clWhite);
Unmap(X, Y, LastX, LastY);
ColorizeCell(clYellow);
end;
end.
2009. május 9., szombat
Swapping two variables without pointers or a third one
Problem/Question/Abstract:
How to swap two integers without a temporary variable or pointers?
Answer:
Solve 1:
Yes, it�s possible to swap the values of two variables without using a third one or swapping pointers.
How??? The answer is: using xor!!!
Place a label and a button on a form, and put this on Button�s click event.
procedure TForm1.Button1Click(Sender: TObject);
var
var1: integer;
var2: integer;
begin
var1 := 19;
var2 := 564;
var1 := var1 xor var2;
var2 := var1 xor var2;
var1 := var1 xor var2;
// They�re swapped!!!
Label1.Caption := 'Var1 = ' + IntToStr(var1) + '; Var2 = ' + IntToStr(var2);
end;
Solve 2:
// ========================================
// This is a FAST swap routine that swaps
// the contents of any 2 variables.
// The variables may be of any type but
// the sizeof the VARS must be passed in Len
//
// eg. X1,X2 : integer;
//
// SwapMem(X1,X2,SizeOf(Integer));
//
// ======================================== }
procedure SwapMem(var Source, Dest; Len: integer);
begin
asm
push edi
push esi
mov esi,Source
mov edi,Dest
mov ecx,Len
cld
@1:
mov al,[edi]
xchg [esi],al
inc si
stosb
loop @1
pop esi
pop edi
end;
end;
2009. május 8., péntek
Open a local HTML file with HTML component
Problem/Question/Abstract:
Open a local HTML file with HTML component
Answer:
The following snippet shows what does not work and how to do it instead:
begin
// this one does not work:
HTML1.RequestDoc('file://C:\help.htm');
// these two will do the trick:
HTML1.RequestDoc('file:///C:\help.htm');
HTML1.RequestDoc('file://localhost/C:\help.htm');
end.
2009. május 7., csütörtök
Sending data from database by portions
Problem/Question/Abstract:
Some times we need send hugo quantity of data from MiddleWare Server to client application. If we do it one portion than user mast wait long time, but we can send this data by portions, when user is needed in it.
Answer:
Some times we need send hugo quantity of data from MiddleWare Server to client application. If we do it one portion than user mast wait long time, but we can send this data by portions, when user is needed in it.
This is sample about getting portion of data from DataSetProvider and adding it to CLientDataSet.
In my article "Accessing DataBase via 3th server" you can read how send data between applications usin INDY components.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Buttons, DB, DBTables, Provider, DBClient, Grids,
DBGrids, ExtCtrls, DBCtrls, IdBaseComponent,
IdAntiFreezeBase, IdAntiFreeze, JvSpecialProgress;
type
TForm1 = class(TForm)
DataSetProvider1: TDataSetProvider;
Table1: TTable;
BitBtn1: TBitBtn;
DBGrid1: TDBGrid;
DBGrid2: TDBGrid;
DataSource1: TDataSource;
DataSource2: TDataSource;
ClientDataSet1: TClientDataSet;
BitBtn2: TBitBtn;
DBNavigator1: TDBNavigator;
IdAntiFreeze1: TIdAntiFreeze;
BitBtn3: TBitBtn;
JvSpecialProgress1: TJvSpecialProgress;
procedure BitBtn1Click(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure BitBtn2Click(Sender: TObject);
procedure DBNavigator1Click(Sender: TObject; Button: TNavigateBtn);
procedure BitBtn3Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
data: OleVariant;
records: integer;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.BitBtn1Click(Sender: TObject);
var
recs: integer;
j: integer;
begin
ClientDataSet1.Active := False;
data := DataSetProvider1.GetRecords(1, recs, ResetOption + MetaDataOption);
Showmessage(format('get %d Records', [recs, i]));
ClientDataSet1.AppendData(data, False);
ClientDataSet1.Active := True;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
records := DataSetProvider1.DataSet.RecordCount;
end;
procedure TForm1.BitBtn2Click(Sender: TObject);
var
recs: integer;
j: integer;
begin
data := DataSetProvider1.GetRecords(-1, recs, ResetOption + MetaDataOption);
ClientDataSet1.AppendData(data, False);
ClientDataSet1.Active := True;
end;
procedure TForm1.DBNavigator1Click(Sender: TObject; Button: TNavigateBtn);
var
recs: integer;
begin
case Button of
nbNext:
begin
if DBNavigator1.DataSource.DataSet.RecordCount data :=
DataSetProvider1.GetRecords(1, recs, ResetOption + MetaDataOption);
ClientDataSet1.AppendData(data, False);
end;
end;
nbPrior:
begin
end;
nbLast:
begin
if DBNavigator1.DataSource.DataSet.RecordCount data :=
DataSetProvider1.GetRecords(-1, recs, ResetOption + MetaDataOption);
ClientDataSet1.AppendData(data, False);
end;
end;
nbFirst:
begin
end;
end;
end;
procedure TForm1.BitBtn3Click(Sender: TObject);
var
recs: integer;
begin
JvSpecialProgress1.Maximum := records;
JvSpecialProgress1.Position := 1;
while ClientDataSet1.RecordCount data := DataSetProvider1.GetRecords(1, recs,
ResetOption + MetaDataOption);
ClientDataSet1.AppendData(data, False);
JvSpecialProgress1.Position := JvSpecialProgress1.Position + 1;
end;
end;
end.
2009. május 6., szerda
New to Delphi 5 - some properties are missing
Problem/Question/Abstract:
I just found out that Ctrl3D property is not showing in IDE in all components.I tried with TDBEdit, TEdit, TStringGrid. The code has Ctrl3D published, but it wouldn't show on any of components in Delphi 5.
Answer:
The Object Inspector can now hide properties based on categories. The Ctl3D property is part of the "Legacy" category, which is hidden by default.
Right click on the Object Inspector, and select the View submenu. Click on 'Legacy' to show the Ctl3D property. You can also select 'All' to show all properties.
You can tell exactly how many properties are currently hidden by looking closely at the statusbar at the bottom of the D5 Object Inspector window.
2009. május 5., kedd
How to get a list of all available Truetype fonts
Problem/Question/Abstract:
I have a situation where I'd like to loop through about 100 font files and extract their friendly name from the file. Has anyone ever done this?
Answer:
Assuming that all fonts are already installed, you need to use EnumFontFamilies and a callback function:
{the callback function prototype}
function FontEnumProc(LogFont: PEnumLogFont; TextMetrics: PNewTextMetric;
FontType: Integer; lParam: LPARAM): Integer; stdcall;
implementation
function FontEnumProc(LogFont: PEnumLogFont; TextMetrics: PNewTextMetric;
FontType: Integer; lParam: LPARAM): Integer; stdcall;
begin
{add the font name and its font type to a list box}
Form1.ListBox1.Items.AddObject(TEnumLogFont(LogFont^).elfLogFont.lfFaceName,
TObject(FontType);
{continue enumeration}
Result := 1;
end;
procedure TForm1.FormClick(Sender: TObject);
begin
EnumFontFamilies(Form1.Canvas.Handle, nil, @FontEnumProc, 0);
end;
If the fonts are not installed, you can install them temporarely.
2009. május 4., hétfő
Show images in the cells of a TStringGrid
Problem/Question/Abstract:
How to show images in the cells of a TStringGrid
Answer:
Solve 1:
The following example uses a TStringGrid to display the bitmaps from the filename strings stored in each cell. In the TStringGrid, I set the DefaultRowHeight to 96 and the DefaultColWidth to 128 (with ColCount = 1).
Here's the OnDrawCell that does all the work:
procedure TForm1.StringGridImageSourceDrawCell(Sender: TObject;
Col, Row: Integer; Rect: TRect; State: TGridDrawState);
var
Bitmap: TBitmap;
Filename: string;
begin
Filename := (Sender as TStringGrid).Cells[Col, Row];
if Filename <> NoImagesLoaded then {special "kludge" case}
begin
Bitmap := TBitmap.Create;
try
Bitmap.LoadFromFile(Filename);
(Sender as TStringGrid).Canvas.StretchDraw(Rect, Bitmap);
finally
Bitmap.Free
end;
{Draw blue outline around selected row}
if Row = (Sender as TStringGrid).Row then
begin
with (Sender as TStringGrid).Canvas do
begin
Pen.Color := clBlue;
Pen.Width := 5;
MoveTo(Rect.Left + 2, Rect.Top + 2);
LineTo(Rect.Right - 2, Rect.Top + 2);
LineTo(Rect.Right - 2, Rect.Bottom - 2);
LineTo(Rect.Left + 2, Rect.Bottom - 2);
LineTo(Rect.Left + 2, Rect.Top + 2)
end;
end;
end;
end;
Solve 2:
In your StringGrid's OnDrawCell event handler, place some code that resembles:
with (Sender as TStringGrid) do
with Canvas do
begin
{...}
Draw(Rect.Left, Rect.Top, Image1.Picture.Graphic);
{...}
end;
Using the Draw() or StretchDraw() method of TCanvas should do the trick. BTW, Image1 above is a TImage with a bitmap already loaded into it.
2009. május 3., vasárnap
How to draw a bitmap between the checkbox and the label in a TCheckListBox
Problem/Question/Abstract:
How to draw a bitmap between the checkbox and the label in a TCheckListBox
Answer:
This should do the trick. It is also possible to place a bitmap in the middle, i.e. The checkbox, then some text, the graphic, then more text, on the same line. The Checklistbox style must be set to lbOwnerDrawVariable for this to work.
procedure TForm1.Button2Click(Sender: TObject);
begin
{Bit1 is called from a resource file}
CheckListBox1.Items.AddObject('Test this bitmap', Bit1);
end;
procedure TForm1.CheckListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
Bitmap: TBitmap;
Offset: Integer;
begin
Offset := 12;
with (Control as TCheckListBox).Canvas do
begin
FillRect(Rect);
Bitmap := TBitmap(CheckListBox1.Items.Objects[Index]);
if Bitmap <> nil then
begin
BrushCopy(Bounds(Rect.Left + 2, Rect.Top + 2, Bitmap.Width, Bitmap.Height),
Bitmap, Bounds(0, 0, Bitmap.Width, Bitmap.Height), clRed);
Offset := Bitmap.width + 8;
end;
TextOut(Rect.Left + Offset, Rect.Top, CheckListbox1.Items[Index])
end;
end;
procedure TForm1.CheckListBox1KeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
height := 16;
end;
2009. május 2., szombat
Block-mode in the editor
Problem/Question/Abstract:
Block-mode in the editor
Answer:
In order to copy/delete rectangles ('columns') of text in the editor, press ALT, then use the left mousebutton to mark the rectangle. Afterwards, you can insert this text rectangle as a block!
2009. május 1., péntek
How to join multiple tables
Problem/Question/Abstract:
I am trying to connect a table in Delphi 3 (using Paradox) to receive data from multiple tables. Up till now, I thought using LEFT OUTER JOIN would do the trick, unfortunately, as soon as I hook up the third table, no data is send to the grid. The join has to be in a way of lookup.
Answer:
If your tables have the same struct (or you can force the same structure via SELECT) you can use UNION:
SELECT * FROM Table1
UNION
SELECT * FROM Table2
If your tables have different structs, try FULL OUTER JOIN instead of LEFT following from localsql.hlp:
SELECT * FROM customer C
FULL OUTER JOIN orders O
on(C.custno = O.custno)
FULL OUTER JOIN items I
on(O.orderno = I.orderno)
Feliratkozás:
Bejegyzések (Atom)