I have a ScrollBox and I'm adding controls to it at runtime. However, when the controls exceed the ScrollBox height, I want the ScrollBox to scroll all the way to the bottom so that the newly added controls are visible.
Doing some research, I've found something called "ScrollInView" for delphi. Seeing how many (quite a lot) of Delphi methods/functions are available in Free Pascal, do you know of any equivalent to this particular one? If not, can you help me achieve my goal (Auto-scrolling the ScrollBox to the bottom) with a different solution?
Thanks in advance,
Oscar
Something like this?
procedure TForm1.Button1Click(Sender: TObject);
begin
with TEdit.Create(Self) do
begin
Parent := ScrollBox1;
Left := 10;
Top := ScrollBox1.ControlCount * 40;
ScrollBox1.VertScrollBar.Position := Top;
end;
end;
And here is hte simple implementation of the ScrollInView method:
TScrollBoxHelper = class helper for TScrollBox
procedure ScrollInView(AControl: TControl);
end;
implementation
procedure TScrollBoxHelper.ScrollInView(AControl: TControl);
begin
if AControl.Parent = Self then
begin
Self.VertScrollBar.Position := AControl.Top;
Self.HorzScrollBar.Position := AControl.Left;
end;
end;
Usage:
procedure TForm1.Button2Click(Sender: TObject);
begin
ScrollBox1.ScrollInView(ScrollBox1.Controls[3]);
end;
Related
I'm working with
FreePascal 3.2.0,
Lazarus 2.0.12,
GTK2 GUI backend
under Ubuntu Linux 18.04.
I have a strange behavior of a TListBox component.
When I focus a empty ListBox by clicking (SetFocus) and
press the [space] key,
some kind of Edit or SearchField appears (..see image below).
The field vanishes after a short period (3 s).
How can I switch off this "feature".
My current work around is to use a handler for the onEnter event to take away the ListBox focus (full code example see below).
procedure TFrmMain.lbxTestEnter(Sender: TObject);
begin
if mUseWorkAround then begin
if LbxTest.Items.Count = 0 then begin
PnlButton.SetFocus;
exit;
end;
end;
end;
I'm not sure if this is the right way.
Full Code Example:
unit UFrmTestListBox;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls,
StdCtrls;
const
PNL_MESSAGE = 'WORK AROUND IS %sACTIVE! ';
type
{ TFrmMain }
TFrmMain = class(TForm)
BtnToggle: TButton;
GbxList: TGroupBox;
GbxLog: TGroupBox;
LbxTest: TListBox;
MmLog: TMemo;
PnlButton: TPanel;
SplMain: TSplitter;
procedure BtnToggleClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure lbxTestEnter(Sender: TObject);
private
mUseWorkAround: Boolean;
mCounter: Integer;
procedure OutLn(S:String);
public
end;
var
FrmMain: TFrmMain;
implementation
{$R *.lfm}
procedure TFrmMain.FormCreate(Sender: TObject);
begin
mUseWorkAround:= false;
mCounter := 0;
PnlButton.Caption:=Format(PNL_MESSAGE,['NOT ']);
end;
procedure TFrmMain.lbxTestEnter(Sender: TObject);
begin
inc(mCounter);
OutLn('');
OutLn(Format('--- %d -----------------------', [mCounter]));
OutLn('ListBox onEnter event');
if mUseWorkAround then begin
OutLn('Work around is active!');
if LbxTest.Items.Count = 0 then begin
OutLn('ListBox is empty, change focus to the button panel.');
PnlButton.SetFocus;
exit;
end;
end else begin
OutLn('Work around NOT active!');
OutLn('ListBox is empty, press [SPACE] & EditField appears.')
end;
end;
procedure TFrmMain.BtnToggleClick(Sender: TObject);
begin
mUseWorkAround:=not mUseWorkAround;
if mUseWorkAround then
PnlButton.Caption:=Format(PNL_MESSAGE,[''])
else
PnlButton.Caption:=Format(PNL_MESSAGE,['NOT ']);
end;
procedure TFrmMain.OutLn(S:String);
begin
MmLog.Lines.BeginUpdate;
MmLog.Lines.Append(S);
MmLog.Lines.EndUpdate;
MmLog.SelStart := Length(MmLog.Lines.Text);
end;
end.
I would like to write a function that checks if a certain letter is in a certain word.
That is the current function (sry for the german)
function woistderbuchstabe (wort, buchstabe:String):String;
VAR i: Integer;
begin
for i:=1 to length(wort) do
if wort[i]=buchstabe then
showmessage(INTtoSTR(i))
//LB_ausgabe.items.add(INTtoSTR(i));
end;
The way it's written now the function actually works. It shows one or several messages with the position(s) of the letter searched for (the variable "buchstabe") in the word "wort". E.g. for wort=abctc and buchstabe=c it shows 3 and 5.
But if i would write it this way
function woistderbuchstabe (wort, buchstabe:String):String;
VAR i: Integer;
begin
for i:=1 to length(wort) do
if wort[i]=buchstabe then
LB_ausgabe.items.add(INTtoSTR(i));
end;
(remove the showmessage and make the ListBox thing actual code)
then I get the error
Undefined Identifier: 'LB_ausgabe'
This is the complete code of the Unit
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
E_kette: TEdit;
E_buchstabe: TEdit;
B_start: TButton;
LB_ausgabe: TListBox;
procedure B_startClick(Sender: TObject);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function woistderbuchstabe (wort, buchstabe:String):String;
VAR i: Integer;
begin
for i:=1 to length(wort) do
if wort[i]=buchstabe then
showmessage(INTtoSTR(i))
//LB_ausgabe.items.add(INTtoSTR(i));
end;
procedure TForm1.B_startClick(Sender: TObject);
begin
woistderbuchstabe (E_kette.text, E_buchstabe.text);
end;
end.
Pls try to be specific as I'm pretty clueless about Delphi.
Thanks in advance
Function woistderbuchstabe is not a member of your class TForm1... so it doesn't have direct access to it's members unless you specify an instance. I suggest this fix:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
E_kette: TEdit;
E_buchstabe: TEdit;
B_start: TButton;
LB_ausgabe: TListBox;
procedure B_startClick(Sender: TObject);
private
{ Private-Deklarationen }
function woistderbuchstabe (wort, buchstabe:String):String;
public
{ Public-Deklarationen }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function TForm1.woistderbuchstabe (wort, buchstabe:String):String;
VAR i: Integer;
begin
for i:=1 to length(wort) do
if wort[i]=buchstabe then
LB_ausgabe.items.add(INTtoSTR(i));
end;
procedure TForm1.B_startClick(Sender: TObject);
begin
woistderbuchstabe (E_kette.text, E_buchstabe.text);
end;
end.
But you can also just reference your Form1: TForm1 instance (global variable), in your function (I recommend you stick with the OO approach, though):
Form1.LB_ausgabe.items.add(INTtoSTR(i));
PS: Check Pos and PosEx functions too, as they are probably (I never benchmarked) faster solution, since they are asm implemented.
In my opinion, if you might need to use this logic from somewhere else, you need to decouple it from your user interface. You can do this by changing your function to a procedure that accepts a more generic class to populate (like a plain old TStrings) as a parameter. As TStrings is a common base for TComboBox.Items, TListBox.Items, TMemo.Lines and is used in many other places, this seems like the most flexible way to accomplish what you want to do.
procedure woistderbuchstabe (List: TStrings; wort, buchstabe:String);
VAR i: Integer;
begin
for i := 1 to length(wort) do
if wort[i] = buchstabe then
List.Add(InttoStr(i));
end;
This allows you to use the procedure with your TListBox (call it with LB_ausgabe.items, a TMemo, using Memo1.Lines, a TComboBox with Combobox1.Items, a TRichEdit with RichEdit1.Lines, or a plain old TStringList directly with SL.
You can now call it from anywhere you want, such as a TForm.Button1Click(Sender: TObject), using ListBox1.Items, or a standalone method that creates and passes in a TStringList. It's not tied to a specific form, so it's more flexible and able to be reused elsewhere.
I would like to do something like this:
(I want to conserve the functions public so I can access them from other procedures/functions).
The functions are on the same form (frmSequenciador) - I didn't post it for it is huge in its integrity..
function geradorDeVetores():TIntArray;
var
contador: Integer;
vetor: array [1..numMax] of integer;
begin
Randomize;
for contador:=1 to numMax do
begin
if contador = 1 then
vetor[contador]=float_round_down(Random*10);
else vetor[contador]:= ***frmSequenciador.evitaRepeticao(contador, vetor)***;
end;
end;
function evitaRepeticao(pos: integer; vetor:TIntArray):integer;
var
numigual: boolean;
temporario, cont: integer;
begin
numigual:=true;
temporario:= float_round_down(Random*10);
for cont:=1 to pos-1 do
if temporario <> vetor[cont] numigual:=false else numigual:=true;
if numigual=false then evitaRepeticao():=temporario else evitaRepeticao():=***frmSequenciador.evitaRepeticao(pos, vetor)***;
end;
It was a simple matter of removing the frmSequenciador prefix of the functions as Ken White stated.
What I wanted to know is: if two functions are inside the same form unit (thx Jerry), do we need a prefix to call each other?
It seems not. Thanks all!
I have a form with 7 TEdit having name EditPhone1, EditPhone2 and so on.
In the same form I query a DB to get data to fill those TEdits. Of course I cannot know in advance how many results the query will return.
How can I call the various TEdit objects when looping on the rowcount of the query?
Use FindComponent to "convert" a component name to the component itself:
var
Edit: TEdit;
I: Integer;
begin
DataSet.First;
I := 1;
while not DataSet.Eof do
begin
Edit := TEdit(FindComponent(Format('EditPhone%d', [I])));
if Edit <> nil then
Edit.Text := DataSet.FieldValues['PhoneNo'];
DataSet.Next;
Inc(I);
end;
Now, this requires to hard-code the EditPhone%d string into the source which results in all kinds of maintainability issues. For example: consider renaming the edits.
Alternative 1:
To not rely on the component names, you could instead make use of TLama's idea and add all the edits to a list:
uses
... , Generics.Collections;
type
TForm1 = class(TForm)
EditPhone1: TEdit;
...
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FEdits: TList<TEdit>;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FEdits := TList<TEdit>.Create;
FEdits.AddRange([EditPhone1, EditPhone2, EditPhone3, EditPhone4, EditPhone5,
EditPhone6, EditPhone7]);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FEdits.Free;
end;
procedure TForm1.ADOQuery1AfterOpen(DataSet: TDataSet);
var
I: Integer;
begin
DataSet.First;
I := 0;
while (not DataSet.Eof) and (I < FEdits.Count) do
begin
FEdits[I].Text := DataSet.FieldValues['PhoneNo'];
DataSet.Next;
Inc(I);
end;
end;
This still requires some maintenance in case of adding edits in future.
Alternative 2:
You could also loop over all edits in the form to find the ones tagged to be added to the list, instead of adding them each explicitly:
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
begin
FEdits := TList<TEdit>.Create;
for I := 0 to ComponentCount - 1 do
if (Components[I] is TEdit) and (TEdit(Components[I]).Tag = 1) then
FEdits.Add(TEdit(Components[I]));
end;
But keeping those tags up to date is another burden.
Alternative 3:
I suggest you use a TDBGrid which is a data-component. Opening the linked dataset will automatically add all phone numbers to the grid. With some settings, the grid may kind of look like a couple of edits below each other.
You can, for example, use Tag property, to find needed component. Set all you TEdit's tag from 1 to 7 (or more), and find component by:
Var I: Integer;
MyEdit : TEdit;
For I = 0 To Self.ComponentCount - 1 Do
if (Self.Components[I] IS TEdit) AND (Self.Components[I] AS TEdit).Tag = YourTag
MyEdit = (Self.Components[I] AS TEdit);
You can also dynamically create so many TEdits, you need, and assign Tag property on creation, and find it this code later in runtime.
I'd suggest using DBCtrlGrid. You place your controls for one row on it, and it repeats the controls for as many rows as your data set has.
Get query result (usually using .RowCount property of TDataset return)
After getting the number of row, do iteration to make TEdit and set the text property
Here is sample of code:
...
For i:=0 to RowCount do
Begin
A:=TEdit.Create(self);
A.Parent:=AForm;
A.Top:=i*14;
A.Text:=ADataset.Field(i).AsString;
End;
...
How to set entire HTML in MSHTML?
I am trying using this assignment:
(Document as IHTMLDocument3).documentElement.innerHTML := 'abc';
but I got the error:
"Target element invalid for this
operation"
I've also tried using
(Document as IHTMLDocument2).write
but this form only adds HTML into the body section, and I need to replace all the HTML source.
Does somebody have any idea of how I do this?
Thanks in advance.
Here's some of my old code, see if it helps you:
type
THackMemoryStream = class(TMemoryStream);
procedure Clear(const Document: IHTMLDocument2);
begin
Document.write(PSafeArray(VarArrayAsPSafeArray(VarArrayOf([WideString('')]))));
Document.close;
end;
procedure LoadFromStream(const Document: IHTMLDocument2; Stream: TStream);
var
Persist: IPersistStreamInit;
begin
Clear(Document);
Persist := (Document as IDispatch) as IPersistStreamInit;
OleCheck(Persist.InitNew);
OleCheck(Persist.Load(TStreamAdapter.Create(Stream)));
end;
procedure SetHtml(const Document: IHTMLDocument2; const Html: WideString);
var
Stream: TMemoryStream;
begin
Stream := TMemoryStream.Create;
try
THackMemoryStream(Stream).SetPointer(PWideChar(Html), (Length(Html) + 1) * SizeOf(WideChar));
Stream.Seek(0, soFromBeginning);
LoadFromStream(Document, Stream);
finally
Stream.Free;
end;
end;
As an alternative you can also use TEmbededWB which is an extended wrapper around a web browser and has some easy to use methods that provide this functionality.