Resizing borderless form from different constraints than far edges? - winforms

I have a custom form which does not have any type of border. I'm drawing some custom borders of my own instead, which do not extend up to the far edges of the form. Instead, whatever's outside this custom drawn border is transparent, through the use of the form's transparent properties. This leaves a smaller portion of the form to be usable and visible.
I know there are tons of solutions out there to accomplish this, and I've already found the best suited method to do this. However, this method assumes that user will be pointing the mouse along the far edges of the form. I need to limit it to react from within different constraints (for example a smaller sized rect).
Here's the code I found which already works on a next-to-the-edge constraint:
procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
....
procedure TForm1.WMNCHitTest(var Message: TWMNCHitTest);
const
EDGEDETECT = 7; //adjust to suit yourself
var
deltaRect: TRect; //not really used as a rect, just a convenient structure
begin
inherited;
if BorderStyle = bsNone then begin
with Message, deltaRect do begin
Left := XPos - BoundsRect.Left;
Right := BoundsRect.Right - XPos;
Top := YPos - BoundsRect.Top;
Bottom := BoundsRect.Bottom - YPos;
if (Top<EDGEDETECT)and(Left<EDGEDETECT) then
Result := HTTOPLEFT
else if (Top<EDGEDETECT)and(Right<EDGEDETECT) then
Result := HTTOPRIGHT
else if (Bottom<EDGEDETECT)and(Left<EDGEDETECT) then
Result := HTBOTTOMLEFT
else if (Bottom<EDGEDETECT)and(Right<EDGEDETECT) then
Result := HTBOTTOMRIGHT
else if (Top<EDGEDETECT) then
Result := HTTOP
else if (Left<EDGEDETECT) then
Result := HTLEFT
else if (Bottom<EDGEDETECT) then
Result := HTBOTTOM
else if (Right<EDGEDETECT) then
Result := HTRIGHT
end;
end;
end;
How would I go about changing the bounds for this to react? For example, the left and right edges should react 10 pixels into the form. The standard form rect may be (0, 0, 100, 100) but I want this method above to work within bounds of (10, 3, 90, 97)

Actually it would make sense to define two constants instead of the only EDGEDETECT since
you require horizontal and vertical offsets to be different and write it from scratch, but here is a quick patch:
procedure TForm1.WMNCHitTest(var Message: TWMNCHitTest);
const
EDGEDETECT = 17; //adjust to suit yourself // <- increased to suit outer offset
var
deltaRect: TRect; //not really used as a rect, just a convenient structure
OuterRect: TRect; // used as a rect
begin
inherited;
if BorderStyle = bsNone then begin
with Message, deltaRect do begin
..
else if (Right<EDGEDETECT) then
Result := HTRIGHT;
..
OuterRect := BoundsRect; // patch
InflateRect(OuterRect, -10, -3);
if not PtInRect(OuterRect, SmallPointToPoint(Message.Pos)) then
Message.Result := HTTRANSPARENT;
end;
end;
end;

Related

How to output an array that has nested arrays in Pascal

I'm creating a basic concept of a music player using Pascal, but I'm struggling to display the albums inside it. The error I got says "(134, 29) Error: Can't read or write variables of this type". I'm assuming it's saying that because I'm using an array within an array, and it's having a hard time displaying both at the same time (although I only want it to display the albums, not the tracks as well).
Here's what my code looks like:
function ReadAllTrack(prompt: String): Tracks;
var
i: Integer;
trackArray: Array of Track;
trackCount: Integer;
begin
WriteLn(prompt);
trackCount := ReadIntegerGreaterThan1('Please enter the number of tracks you would like to add: ');
Setlength(trackArray, trackCount);
for i := 0 to trackCount - 1 do
begin
WriteLn('Enter the details for your track:');
trackArray[i] := ReadTrack();
end;
result := trackArray;
end;
function ReadAlbum(): Album;
begin
result.albumName := ReadString('Album name: ');
result.artistName := ReadString('Artist name: ');
result.albumGenre := ReadGenre('Genre:');
result.trackCollection := ReadAllTrack('Track Collection:');
end;
function ReadAllAlbums(): Albums;
var
i: Integer;
albumArray: Array of Album;
albumCount: Integer;
begin
albumCount := ReadIntegerGreaterThan1('Please enter the number of albums you would like to add: ');
Setlength(albumArray, albumCount);
for i := 0 to albumCount - 1 do
begin
WriteLn('Enter the details for your album:');
albumArray[i] := ReadAlbum();
end;
result := albumArray;
end;
procedure DisplayAlbumOptions(listOfAllAlbums: Albums);
var
userInput: Integer;
begin
WriteLn('1. Display all albums');
WriteLn('2. Display all albums for a genre');
userInput := ReadIntegerRange('Please enter a number (1, 2) to select: ', 1, 2);
case userInput of
1: WriteLn(listOfAllAlbums); //Error: Can't read or write variables of this type
end;
end;
Basically what this does is it will ask the user showing 5 options:
1. Add albums
2. Display albums
etc
If the user selects 1, the program will ask the user to input the number of albums they want to input. Then for each album it'll ask them to enter the details, and then the tracks.
Then if the user selects 2, the program will ask the user to choose either display every single album there is, or display all albums for a single genre (I'll be working on this one after solving this problem). At first I thought it would be just as simple as WriteLn(TheAlbumArray); but turns out it was more complicated than I thought because I don't think it's possible for the program to display it this way. I tried separating the albums and tracks so that it would only display the albums when I use WriteLn(TheAlbumArray); but it wasn't possible because the tracks still have to be "inside" the album so that when I display the albums and select one of them, it would then display the tracks....
Any help or suggestion for this and/or the second will be much appreciated ^^
Your original question contained a lot of superfluous detail. After the edit, you removed the type declarations, but kept much of the superfluous detail.
However, it is possible to discern the problem you are passing an array of record to Writeln. The Writeln function can accept only certain simple types as arguments, e.g. strings, numerical types, boolean. You certainly cannot pass an array to Writeln. You must iterate over the array and process each member individually.
So you might try
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
WriteLn(listOfAllAlbums[i]);
But that does not work either, because listOfAllAlbums[i] is a record, and a record is a compound type which cannot be passed to Writeln. So you need to process the record separately. If you want to display just the title, then you write:
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
WriteLn(listOfAllAlbums[i].albumName);
If you want to print the track titles too then you need to iterate over the array contained in the record.
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
begin
WriteLn(listOfAllAlbums[i].albumName);
for j := low(trackCollection) to high(trackCollection) do
WriteLn(listOfAllAlbums[i].trackCollection[j]);
end;
It is impossible to use composite types (arrays, records, ...) in Read[ln] and Write[ln] procedures.
To make your code more transparent you could to create type helper for your array(s) and use well-known AsString property. Here is example for simple array of Integer:
program foo;
{$mode objfpc}{$H+}
{$modeswitch typehelpers}
uses
Classes, SysUtils;
type
TMyArray = array of Integer;
TMyArrayHelper = type helper for TMyArray
private
function GetAsString: string;
procedure SetAsString(const AValue: string);
public
property AsString: string read GetAsString write SetAsString;
end;
function TMyArrayHelper.GetAsString: string;
var
i: Integer;
begin
Result := '';
for i in Self do
begin
if Result <> '' then
Result := Result + ', ';
Result := Result + IntToStr(i);
end;
Result := '[' + Result + ']';
end;
// Relatively simple parser
// Fill free to implement ones for your array type
procedure TMyArrayHelper.SetAsString(const AValue: string);
var
tmp, s: string;
items: TStringArray;
i: Integer;
begin
tmp := Trim(AValue);
if not (tmp.StartsWith('[') and tmp.EndsWith(']')) then
raise Exception.CreateFmt('Invalid array literal format: "%s"', [tmp]);
tmp := tmp.Trim(['[', ']']);
items := tmp.Split([',']);
for s in items do
try
StrToInt(s);
except
on e: Exception do
raise Exception.CreateFmt('Invalid integer literal: "%s"', [s]);
end;
SetLength(Self, Length(items));
for i := 0 to Length(items) - 1 do
Self[i] := StrToInt(items[i]);
end;
var
a1, a2: TMyArray;
begin
a1.AsString := '[1,2,3,5]';
Writeln('a1 = ', a1.AsString);
a2.AsString := a1.AsString;
a2[1] := 999;
Writeln('a2 = ', a2.AsString);
end.
Helper types in FreePascal
TStringHelper in SysUtils unit

Hide mouse pointer / cursor when playing video / slideshow in Inno Setup

I created an Inno Setup script that plays a slideShow or a video during program installation depending on what I choose to play.
When I bring mouse to the area which the playback is doing during the slideshow / video playback, a cursor (crArrow) is appearing on video / slideshow.
I want to prevent the mouse cursor from being displayed on video / slideshow while the slideshow / video is playing.
When using crNone property for the handling form window (BackgroundForm) the cursor is hiding only from it and not from slideshow / video. Isn't there anyway I can hide the cursor from slideshow/video? How can I apply crNone for that? I mean like SlideShow.crNone or Video.crNone.
I attached two images showing how the cursor appearing.
The Cursor appearing when playing SlideShow.
The Cursor appearing when playing Video.
How I handle video on the BackgroundForm using Inno Media Player:
procedure PlayMPEGVideo();
begin
if VBRadio2.Checked then begin
if FileExists(ExpandConstant('{tmp}\Video.mp4')) then
begin
if DSInitializeVideoFile(ExpandConstant('{tmp}\Video.mp4'), BackgroundForm.Handle, Width, Height, #BackgroundVideoPlay) then
begin
BackgroundForm.Width := GetSystemMetrics(0);
BackgroundForm.Height := GetSystemMetrics(1);
BASS_Pause;
SoundCtrlButton.Enabled := False;
DSSetVolume(-0);
DSPlayMediaFile;
WizardForm.BringToFront;
PauseBT.Show;
PlayBT1.hide;
PlayBT.hide;
with WizardForm do begin
WizardForm.NextButton.Caption := 'Install';
end;
end;
end;
end else begin
with WizardForm do begin
if CurPageID = wpInstalling then begin
PauseBT.hide;
CompactCheckBox.Visible := False;
WizardForm.WizardSmallBitmapImage.Show;
WizardForm.Bevel1.Show;
with WizardForm do begin
WizardForm.ProgressGauge.show;
end;
end;
end;
end;
end;
How I handle slideshow on the BackgroundForm using isSlideShow:
procedure MakeSlideShow();
var
i :integer;
begin
if NoBackgroundCheckBox.Checked = True then begin
with WizardForm do begin
if CurPageID=wpInstalling then begin
PauseBT.hide;
CompactCheckBox.Visible := False;
WizardForm.WizardSmallBitmapImage.Show;
WizardForm.Bevel1.Show;
with WizardForm do begin
WizardForm.ProgressGauge.show;
end;
end;
end;
end else begin
BackgroundForm:= TForm.Create(nil);
BackgroundForm.BorderStyle:= bsNone;
BackgroundForm.Color:=clBlack;
BackgroundForm.SetBounds(0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
BackgroundForm.Visible:=True;
BackgroundForm.enabled:= False;
PicList:=tstringlist.Create;
#ifexist "Slides\1.jpg"
#sub ExtractFile
ExtractTemporaryFile('{#i}.jpg');
#endsub
#for {i = 1; FileExists(StringChange("Slides\FileName.jpg", "FileName", Str(i))) != 0; i++} ExtractFile
#endif
i:=1;
repeat
piclist.add(ExpandConstant('{tmp}\'+IntToStr(i)+'.jpg'));
i:=i+1;
until FileExists(ExpandConstant('{tmp}\'+IntToStr(i)+'.jpg')) = False;
BackgroundForm.Show;
InitializeSlideShow(BackgroundForm.Handle, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), true, 1);
ShowImage(ExpandConstant('{tmp}') + '\1.jpg', 1);
PlayBT1 := PlayBT;
end;
end;
Thanks in advance.
In general, to hide a mouse cursor, set the .Cursor property of a control to crNone.
For Inno Media Player: There's no "video" control exposed by its API. You would have to modify its source code and recompile. Particularly, you need to call the IVideoWindow::HideCursor method on the FVideoWindow in the TDirectShowPlayer.InitializeVideoWindow.
const
OATRUE = -1;
procedure TDirectShowPlayer.InitializeVideoWindow(WindowHandle: HWND; var Width,
Height: Integer);
begin
ErrorCheck(FGraphBuilder.QueryInterface(IVideoWindow, FVideoWindow));
ErrorCheck(FVideoWindow.HideCursor(OATRUE));
...
end;
Note that it does not work, when the parent window (the BackgroundForm) is disabled. So you cannot set the BackgroundForm.Enabled := False.
To prevent the background/video window from getting activated, handle the TForm.OnActive by returning focus back to the wizard form:
procedure BackgroundFormActivated(Sender: TObject);
begin
WizardForm.BringToFront;
end;
...
begin
...
BackgroundForm:= TForm.Create(nil);
...
BackgroundForm.OnActivate := #BackgroundFormActivated;
end;
This is a complete code that works for me - hides the cursor over the background video - when using the recompiled MediaPlayer.dll with the HideCursor call, provided by you - tested on Windows 10.
var
BackgroundForm: TForm;
procedure OnMediaPlayerEvent(EventCode, Param1, Param2: Integer);
begin
{ noop }
end;
procedure BackgroundFormActivated(Sender: TObject);
begin
WizardForm.BringToFront;
end;
procedure PlayMPEGVideo();
var
Width, Height: Integer;
begin
BackgroundForm := TForm.Create(nil);
BackgroundForm.BorderStyle := bsNone;
BackgroundForm.Color := clBlack;
BackgroundForm.Visible := True;
BackgroundForm.Cursor := crNone;
BackgroundForm.OnActivate := #BackgroundFormActivated;
Width := GetSystemMetrics(0);
Height := GetSystemMetrics(1);
BackgroundForm.SetBounds(0, 0, Width, Height)
if DSInitializeVideoFile(
'...\video.avi', BackgroundForm.Handle, Width, Height, #OnMediaPlayerEvent) then
begin
DSPlayMediaFile;
WizardForm.BringToFront;
end;
end;
For isSlideShow: I didn't find any documentation or source code for this.

What was picked within a columns.picklist?

I wasted a bit of time trying to work out something I figured would be simple.
I've got a database with multiple tables (MySQL). One table containing "Components" and another containing "Products". Products are built using Components, for example; Product ABC might be made up of 3 x Screws, 4 x bolts, 1 kilogram of fresh air... etc! Am I making sense so far?
The components are displayed in a DBGrid. If the user makes a mistake and wants to add another "Component" to a "Product" a Picklist appears listing all Components (from a different table) for them to select from.
Now, here's my problem! When something is selected from the column[i].picklist (this is part of a DBGrid) how do I know what was selected. I thought there would be an event fired, but there doesn't seem to be.
I need to know which item was selected so I can retrieve an appropriate description for the next field.
There are 3 fields, they are COMPONENT, DESCRIPTION, QUANTITY. Only COMPONENT and QUANTITY can be edited by the user.
I hope I'm making some sense here.
Here is the code I'm using now (as messy as it is);
procedure TForm1.CompletePolesSourceStateChange(Sender: TObject);
var
loop: Integer;
Tmp: string;
begin
case CompletePolesSource.state of
dsInsert:
begin
CompVals.Clear; // Is a tstringlist created elsewhere
CompVals.Delimiter := '|';
CompVals.QuoteChar := '"';
PoleComponentsGrid.Columns[0].readonly := false; // Is readonly when not in DSInsert
PoleComponentsGrid.Columns[0].PickList.Clear; // Clear other crap
{
Now add Parts to a Name / Value list (CODE / DESCRIPTION) so I can later
get the description without looking it up in the other table.
}
for loop := 1 to componentstable.RecordCount do // Get CODE from other table
begin
componentstable.RecNo := loop;
tmp := componentstable.Fieldbyname('CODE').asstring + '=' + componentstable.Fieldbyname('ITEM').asstring;
CompVals.Add(tmp);
PoleComponentsGrid.Columns.Items[0].PickList.Add(tmp);
end;
PoleComponentsGrid.Columns.Items[0].readonly := true;
end;
end;
end;
This will show the data of the selected rows of the DBGrid
procedure TFrmPrincipal.btnShowSelectedRowsClick(Sender: TObject);
var
i: Integer;
aux: string;
begin
for i := 0 to DBGrid1.SelectedRows.Count - 1 do
begin
ClientDataSet1.GotoBookmark(pointer(DBGrid1.SelectedRows.Items[i]));
aux := aux + IntToStr(ClientDataSet1.RecNo) + ' - ' +
ClientDataSet1.FieldByName('CUSTOMER').AsString + #13;
end;
ShowMessage(‘Selected Rows: ‘ + #13 + aux);
end;

Inno Setup: Combo box closing immediately on drop-down (which starts an application)

I am getting a list of Application Pools and then populating the combo box with the names of app pools. The problem is that when when the OnDropDown event is called the combo box opens for a fraction of a second and then closes straight away. It does not remain "dropped down". I do see all the app pools in the combo box. Here is my code:
function GetApplicationPoolList() : TArrayOfString;
var
i, nExitCode: Integer;
sFileLines: TArrayOfString;
sTempFileName, sListAppPoolCmd: String;
begin
sListAppPoolCmd := ExpandConstant('{sys}') + '\inetsrv\appcmd list apppool /text:name';
sTempFileName := ExpandConstant('{tmp}') + '\appPoolList.txt';
if not ExecAppCmd(Format('%s > %s',[sListAppPoolCmd, sTempFileName]), nExitCode) then begin
MsgBox('Could not get app pools', mbError, MB_OK);
end else begin
LoadStringsFromFile(sTempFileName, sFileLines);
end
Result := sFileLines;
end;
// ==============================================
procedure OnAppPoolComboBoxDropDown(Sender: TObject);
var
sAppPoolList: TArrayOfString;
i: Integer;
begin
// Clear existing
appPoolComboBox.Items.Clear;
// Populate the combo box with the application pools
sAppPoolList := GetApplicationPoolList;
For i := 0 to GetArrayLength (sAppPoolList) - 1 do
begin
// ComboBox with Application Pool Names
appPoolComboBox.Items.Add(sAppPoolList[i]);
end;
appPoolComboBox.ItemIndex := 0;
end;
function ExecAppCmd(params :String; nExitCode: Integer) :Boolean;
var
execSuccessfully :Boolean;
resultCode :Integer;
begin
execSuccessfully := Exec('cmd.exe', '/c ' + '' + ' ' + params, '', SW_HIDE, ewWaitUntilTerminated, resultCode);
nExitCode := resultCode;
Result := execSuccessfully and (resultCode = 0);
end;
I am not sure what is happening here. Any advice is appreciated.
EDIT: ExecAppCmd seems to be the issue, commenting it out makes the combo box behave normally... Though not sure why
The drop-down closes probably because the combo box loses focus momentarily as the application is starting.
I'd say it's a bad practice to call an application on drop-down, as it takes time and it ruins a user experience. Populate the combo box earlier, like from CurPageChanged.

Delphi TeeChart only showing one record from dataset

Using Delphi Steema TeeChart component, if I link a BarSeries to a dataset using the user interface, it shows up fine, but if I do it using code (which I need to), it's only showing one bar, even when I have several records in the database. What am I doing wrong?
Code:
var
i:Integer;
Bar:TBarSeries;
begin
ADataSet.Close;
ADataSet.LoadFromDataSet(mtbl);
ADataSet.Active := true;
ADataSet.First;
ASource.DataSet := ADataSet;
Bar := TBarSeries.Create(AChart);
Bar.Assign(Series2);
Bar.ParentChart := AChart;
Bar.DataSource := ASource;
Bar.XLabelsSource := 'Date';
Bar.YValues.ValueSource := 'Load';
for i := 0 to AChart.SeriesCount - 1 do
begin
AChart.Series[i].CheckDataSource;
end;
ADataSet is a DevExpress MemData (TdxMemData). When I run the program, the X axis is only showing one bar, the first record in the dataset, even though I have 4 records in the dataset.
This code works for me (using an Access database with fields ID and Height, I dropped a TDBChart, TADODataSet, and a TButton on a form):
procedure TForm1.Button1Click(Sender: TObject);
var
Bar : TBarSeries;
begin
ADODataSet1.Close;
ADODataSet1.ConnectionString := 'Provider=Microsoft.Jet.OLEDB.4.0;...';
Bar := TBarSeries.Create(DBChart1);
DBChart1.AddSeries(Bar);
Bar.ParentChart := DBChart1;
Bar.DataSource := ADODataSet1;
Bar.XLabelsSource := 'ID';
Bar.YValues.ValueSource := 'Height';
ADODataSet1.Active := true;
end;
Note that the Datasource should be a TTable, TQuery, or TDataSet (not a TDataSource - go figure!).
Hope this helps.
TChart refreshes the query each time you set
ADataSet.Active := true;
so, move this command to the end of your block (e.g. after you've set up the series properties).

Resources