Delphi 10.2.2 mobile
Starting with a blank mobile project, I drop a TListBox on the form. I add two TListBoxItems.
procedure TForm1.ListBox1Click(Sender: TObject);
begin
ShowMessage('ListBoxItem.itemindex = ' + ListBox1.ItemIndex.ToString);
end;
When I click on the first item in Windows and Macintosh, the OnClick() correctly reports that item index 0 has been clicked.
When I click on the first item in mobile (iOS and Android) the OnClick() reports the item index as -1 (not 0 as it should). Then it goes on to highlight the first item.
If I then click on the second item in mobile, the OnClick() reports the item index as 0 (not 1 as it should). Then it goes on to highlight the second item.
How can I get the correct item in OnClick() when clicking in a TListBox on mobile?
Clearly the OnClick event is being triggered before the ItemIndex is updated. So you will have to delay processing until after the ItemIndex has a chance to be updated first. You can:
use TThread.ForceQueue() (10.2 Tokyo+ only):
procedure TForm1.ListBox1Click(Sender: TObject);
begin
TThread.ForceQueue(nil,
procedure
begin
ShowMessage('ListBoxItem.itemindex = ' + ListBox1.ItemIndex.ToString);
end
);
end;
use TThread.Queue():
procedure TForm1.ListBox1Click(Sender: TObject);
begin
TThread.CreateAnonymousThread(
procedure
begin
TThread.Queue(nil,
procedure
begin
ShowMessage('ListBoxItem.itemindex = ' + ListBox1.ItemIndex.ToString);
end
);
end
).Start;
end;
use a short timer:
procedure TForm1.ListBox1Click(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
ShowMessage('ListBoxItem.itemindex = ' + ListBox1.ItemIndex.ToString);
end;
Related
I am currently doing a school project, I am making a Credit Card machine. I need the 'Enter Button' to
run different code when it is clicked. The first click must get the card number from an edit ps... (I clear the edit once the card number has been retrieved), and the second click must get the pin from the same edit.
How would I do this?
procedure TfrmMainMenu.btbtnEnterClick(Sender: TObject);
var
sCvv,sPin:string;
begin
iCount2:=0;
sCardNumber:=lbledtCardInfo.Text;
if (Length(sCardNumber)<>16) AND (iCount2=0) then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
end
else
begin
Inc(iCount2);
lbledtCardInfo.clear;
lbledtCardInfo.EditLabel.Caption:='Enter Pin' ;
btbtnEnter.Enabled:=false;
end; //if
if iCount2=2 then
begin
btbtnEnter.Enabled:=true;
sPin:=lbledtCardInfo.Text;
ShowMessage(sPin);//returns a blank
end;
You could try to do everything in a single event handler. There are several different ways to handle that. However, a different solution would be to use separate event handlers for each task, and then each task can assign a new handler for the next click to perform, eg:
procedure TfrmMainMenu.FormCreate(Sender: TObject);
begin
// you can set this at design-time if desired...
btbtnEnter.OnClick := GetCCNumber;
end;
procedure TfrmMainMenu.GetCCNumber(Sender: TObject);
begin
sCardNumber := lbledtCardInfo.Text;
if Length(sCardNumber) <> 16 then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
Exit;
end;
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Pin' ;
btbtnEnter.OnClick := GetCCPin;
end;
procedure TfrmMainMenu.GetCCPin(Sender: TObject);
var
sPin: string;
begin
sPin := lbledtCardInfo.Text;
if Length(sPin) <> 4 then
begin
ShowMessage('Card Pin has to 4 digits,please try again!!');
Exit;
end;
ShowMessage(sPin);
...
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Number' ;
btbtnEnter.OnClick := GetCCNumber;
end;
A variation of this would be to create multiple buttons that overlap each other in the UI, and then you can toggle their Visible property back and forth as needed, eg:
procedure TfrmMainMenu.FormCreate(Sender: TObject);
begin
// you can set this at design-time if desired...
btbtnCCPinEnter.Visible := False;
btbtnCCNumEnter.Visible := True;
end;
procedure TfrmMainMenu.btbtnCCNumEnterClick(Sender: TObject);
begin
sCardNumber := lbledtCardInfo.Text;
if Length(sCardNumber) <> 16 then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
Exit;
end;
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Pin' ;
btbtnCCNumEnter.Visible := False;
btbtnCCPinEnter.Visible := True;
end;
procedure TfrmMainMenu.btbtnCCPinEnterClick(Sender: TObject);
var
sPin: string;
begin
sPin := lbledtCardInfo.Text;
if Length(sPin) <> 4 then
begin
ShowMessage('Card Pin has to 4 digits,please try again!!');
Exit;
end;
ShowMessage(sPin);
...
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Number' ;
btbtnCCPinEnter.Visible := False;
btbtnCCNumEnter.Visible := True;
end;
Notice that you test iCount2 = 0 immediately after setting iCount2 := 0. Thus, that test will always be True. Furthermore, the later test iCount2 = 2 will always be False because the value starts at 0 and you only have one Inc in between.
Instead try the following.
Add two string fields FCardNumber and FPin to your form class:
private
FCardNumber: string;
FPin: string;
Also create an enumerated type TEntryStage = (esCardNumber, esPin) and add a field of this type. This will make your code look like this:
private
type
TEntryStage = (esCardNumber, esPin);
var
FCardNumber: string;
FPin: string;
FEntryStage: TEntryStage;
In Delphi, class fields (class member variables) are always initialized, so FEntryStage will be esCardNumber (=TEntryStage(0)) when the form is newly created.
Add a TLabeledEdit (I see you use those) and a TButton; name them eInput and btnNext, respectively. Let the labeled edit's caption be Card number: and the caption of the button be Next.
Now add the following OnClick handler to the button:
procedure TForm1.btnNextClick(Sender: TObject);
begin
case FEntryStage of
esCardNumber:
begin
// Save card number
FCardNumber := eInput.Text;
// Prepare for the next stage
eInput.Clear;
eInput.EditLabel.Caption := 'Pin:';
FEntryStage := esPin;
end;
esPin:
begin
// Save pin
FPin := eInput.Text;
// Just do something with the data
ShowMessageFmt('Card number: %s'#13#10'Pin: %s', [FCardNumber, FPin]);
end;
end;
end;
You might notice that you cannot trigger the Next button using Enter, which is very annoying. To fix this, do
procedure TForm1.eInputEnter(Sender: TObject);
begin
btnNext.Default := True;
end;
procedure TForm1.eInputExit(Sender: TObject);
begin
btnNext.Default := False;
end;
Much better!
So basically when a user clicks on the checkbox, I want to add that item in my list, I have tried using the OnChange event but this is not working for me as it gets fired even when the Checkbox is not clicked.
My code is simple and straightforward
procedure LvUserChange(Sender: TObject; Item: TListItem;Change: TItemChange);
var
objUser : TUsers;
begin
if not assigned(objListOfChangedUsers) then
objListOfChangedUsers := TObjectList.Create;
objUser := Item.Data;
objListOfChangedUsers.Add(objUser);
end;
I want this code to be fired ONLY when the checkbox is clicked in the ListView
In Delphi 2009 and later, you can use the TListView.OnItemChecked event to detect checkbox clicks. Delphi 2007 does not have such an event, in which case you will need to detect the notification manually in your own code. I will demonstrate with an interposer class, but there are other ways to do this.
uses
..., CommCtrl, ComCtrls, ...;
type
TListView = class(ComCtrls.TListView)
protected
procedure CNNotify(var Message: TWMNotifyLV); message CN_NOTIFY;
end;
....
procedure TListView.CNNotify(var Message: TWMNotifyLV);
begin
inherited;
if Message.NMHdr.code = LVN_ITEMCHANGED then
begin
if (Message.NMListView.uChanged = LVIF_STATE) and
( ((Message.NMListView.uOldState and LVIS_STATEIMAGEMASK) shr 12)
<> ((Message.NMListView.uNewState and LVIS_STATEIMAGEMASK) shr 12)) then
begin
// changing check box state will land you here
end;
end;
end;
Since OnItemChecked does not exist, you cannot get your event to fire only when the item is checked, but you can filter your event like this
procedure LvUserChange(Sender: TObject; Item: TListItem;Change: TItemChange);
var
objUser : TUsers;
begin
if Change = ctState then
begin
if Item.Checked then
begin
if not assigned(objListOfChangedUsers) then
objListOfChangedUsers := TObjectList.Create;
objUser := Item.Data;
objListOfChangedUsers.Add(objUser);
end
else
begin
// just in case there are any actions when unchecking
end;
end;
end;
I don't have Delphi 2007 but have checked the documentation and it should work.
I have form with enabled / disabled controls to indicate form is in busy or idle state.
I need to enable only ONE control (a button, but could be else), when it was disabled to abort some process. I change the button caption to 'ABORT'.
I click button A, i change the caption of button A to 'ABORT'. All other control will be disabled, but i want a button with caption 'ABORT' is still enabled.
procedure F1.FormBusy (sender);
var
a: Integer;
begin
for a := 0 to TabSheet1.ControlCount - 1 do
begin
TabSheet1.Controls[a].Enabled := False;
(* if TabSheet1.Controls[a] caption := 'ABORT' then
TabSheet1.Controls[a].Enabled := True
< how to do this ? *)
end;
end;
Usage example :
procedure F1.LB1Click(sender: TObject);
begin
FormBusy(sender);
try
// do something
finally
FormIdle(sender);
end;
end;
Rather than trying to find the button by its Caption property, why not access it directly from the array?
for a := 0 to TabSheet1.ControlCount - 1 do
begin
TabSheet1.Controls[a].Enabled := TabSheet1.Controls[a] = Button1;
end;
Each TControl will be disabled except for Button1 which will be enabled.
You can define another method to assign busy parameter :
procedure F1.MAJIHM(const isBusy : Boolean);
var a: Integer;
begin
for a := 0 to TabSheet1.ControlCount - 1 do
begin
TabSheet1.Controls[a].Enabled := isBusy;
end;
btnABORT.enabled := not isBusy;
end;
procedure F1.FormBusy (sender);
begin
MAJIHM(True);
end;
procedure F1.FormIdle (sender);
begin
MAJIHM(False);
end;
You said:
I click button A, i change the caption of button A to 'ABORT'. All
other control will be disabled, but i want a button with caption
'ABORT' is still enabled.
And from your usage example it is clear that you pass that button to F1.FormBusy() where you can refer to it as the sender parameter:
procedure F1.FormBusy(sender: TObject);
var
a: Integer;
begin
for a := 0 to TabSheet1.ControlCount - 1 do
TabSheet1.Controls[a].Enabled := TabSheet1.Controls[a] = sender;
end;
In the FormIdle() function you simply enable all controls.
I have a Firemonkey multi device project in Delphi 10 Seattle where the user can get a screen at the start of the app. Here the user needs to fill in 2 fields. But when I click on the edit fields the Virtual Keyboard isn't shown. If I skip this screen at start and call it later then the Virtual Keyboard is shown. This is done in the same way too.
I found sort of a solution:
When i click on the edit fields i call show VirtualKeyboard myself. The only problem is that the cursor isn't shown in the edit field.
Is there a way to place the cursor myself? Or does anyone know how to solve the Virtual Keyboard not showing problem in an other way?
Problem is on both Android and iOS
In the code below you can see the initial form create. The problem is when in ConnectFromProfile method the actCreateNewProfileExecute is called. There it will call a new form. In that form(TfrmProfile) the virtual keyboard isn't shown. I also call this form with another action and then it works fine.
procedure TfrmNocoreDKS.FormCreate(Sender: TObject);
begin
Inherited;
System.SysUtils.FormatSettings.ShortDateFormat := 'dd/mm/yyyy';
CheckPhone;
ConnectfromProfile;
if not Assigned(fProfileAction) then
ConnectDatabase
Else
lstDocuments.Enabled := False;
{$IFDEF ANDROID}
ChangeComboBoxStyle;
{$ENDIF}
end;
procedure TfrmNocoreDKS.ConnectfromProfile;
begin
fdmProfileConnection := TdmConnection.Create(nil);
fdmProfileConnection.OpenProfileDb;
fdmProfileConnection.LoadProfiles;
if fdmProfileConnection.Profiles.Count = 0 then
begin // Createdefault Profile
fProfileAction := actCreateNewProfileExecute;
end
else if fdmProfileConnection.Profiles.Count = 1 then
begin // one profile load connection;
fProfileAction := nil;
fCurrentProfile := fdmProfileConnection.Profiles.Items[0];
end
else
begin // multiple profiles choose connection;
fProfileAction := SelectProfileOnStartUp;
end;
end;
procedure TfrmNocoreDKS.FormShow(Sender: TObject);
begin
if Assigned(fProfileAction) then
fProfileAction(Self);
end;
procedure TfrmNocoreDKS.actCreateNewProfileExecute(Sender: TObject);
var
profilename, databasename, pathname: string;
prf: TfrmProfile;
begin
prf := TfrmProfile.Create(nil);
prf.Data := fdmProfileConnection.Profiles;
prf.ShowModal(
procedure(ModalResult: TModalResult)
begin
if ModalResult = mrOk then
begin
profilename := prf.edtProfilename.Text;
databasename := prf.edtDatabaseName.Text;
{$IFDEF IOS}
pathname := System.IOUtils.TPath.GetDocumentsPath;
{$ENDIF}
{$IFDEF ANDROID}
pathname := System.IOUtils.TPath.GetDocumentsPath;
{$ENDIF}
{$IFDEF WIN32}
pathname := ExtractFilePath(ParamStr(0)) + '\Data';
{$ENDIF}
FDSQLiteBackup1.Database := System.IOUtils.TPath.Combine(pathname,
'default.sqlite3'); // Default Database
FDSQLiteBackup1.DestDatabase := System.IOUtils.TPath.Combine(pathname,
databasename + '.sqlite3');
FDSQLiteBackup1.Backup;
fdmProfileConnection.AddProfile(databasename + '.sqlite3', profilename);
fdmProfileConnection.LoadProfiles;
fCurrentProfile := fdmProfileConnection.Profiles.Items[0];
connectDatabase;
end else
Application.Terminate;
end);
end;
Do not show any additional forms in MainForm.OnCreate/OnShow. Trying this on iOS 9.2 freeze app at "launch screen".
Instead of this, show new form asynchronously, like this:
procedure TForm4.FormShow(Sender: TObject);
begin
TTask.Run(procedure
begin
TThread.Synchronize(nil, procedure // work with visual controls - only throught Synchronize or Queue
begin
Form5:=TForm5.Create(Application);
Form5.ShowModal;
end)
end);
end;
of cource, you can separate this code to external procedures:
procedure ShowMyForm;
begin
Form5:=TForm5.Create(Application);
Form5.ShowModal;
end;
procedure TaskProc;
begin
TThread.Synchronize(nil, ShowMyForm);
end;
procedure TForm4.FormShow(Sender: TObject);
begin
TTask.Run(TaskProc);
end;
========
Another way - do not use any additional forms. Create frame and put it (at runtime) on MainForm with Align = Contents. After all needed actions - hide or release (due to ARC dont forget to set nil to frame variable) this frame.
We use mouse left click to trigger actions in menu items of TPopupMenu. How to trigger different action on mouse middle click in these menu items? In other word, mouse left and middle click on TPopupmenu's menu items are both different action.
The global Menus.PopupList variable keeps track of all PopupMenus and handles all massages send to them. You can override this PopupList with your own instance, as follows:
type
TMyPopupList = class(TPopupList)
private
FMenuItem: TMenuItem;
protected
procedure WndProc(var Message: TMessage); override;
end;
{ TMyPopupList }
procedure TMyPopupList.WndProc(var Message: TMessage);
var
FindKind: TFindItemKind;
I: Integer;
Item: Integer;
Action: TBasicAction;
Menu: TMenu;
begin
case Message.Msg of
WM_MENUSELECT:
with TWMMenuSelect(Message) do
begin
FindKind := fkCommand;
if MenuFlag and MF_POPUP <> 0 then
FindKind := fkHandle;
for I := 0 to Count - 1 do
begin
if FindKind = fkHandle then
begin
if Menu <> 0 then
Item := GetSubMenu(Menu, IDItem)
else
Item := -1;
end
else
Item := IDItem;
FMenuItem := TPopupMenu(Items[I]).FindItem(Item, FindKind);
if FMenuItem <> nil then
Break;
end;
end;
WM_MBUTTONUP:
if FMenuItem <> nil then
begin
GetMenuItemSecondAction(FMenuItem, Action);
Menu := FMenuItem.GetParentMenu;
if Action <> nil then
begin
Menu := FMenuItem.GetParentMenu;
SendMessage(Menu.WindowHandle, WM_IME_KEYDOWN, VK_ESCAPE, 0);
Action.Execute;
Exit;
end;
end;
end;
inherited WndProc(Message);
end;
initialization
PopupList.Free;
PopupList := TMyPopupList.Create;
The GetMenuItemSecondAction routine you have to write yourself. Maybe this answer provides some help about adding your own actions to a component.
Note that the code under WM_MENUSELECT is simply copied from Menus.TPopupList.WndProc. You could also retrieve the MenuItem in the WM_MBUTTONUP handling by using MenuItemFromPoint.
But as the many comments have already said: think twice (or more) before implementing this UI functionality.
You are not notified of such an event. If you were there would be an entry for middle mouse button click in the list of menu notifications.
So perhaps you could use some sort of hack behind the back of the menu system if you really want to do this. However, as discussed in the comments, there are good reasons for thinking that your proposed UI may not be very appropriate.
If the middle click is not a suitable choice, how about using some key combination with mouse click like Ctrl-Click, to trigger another action? The TPopupMenu doesn't have any event related to customized click.
That is preferred over a middle mouse button click.
And then it is much simpler. Just check in your action execute handler if the CTRL button is pressed:
procedure TForm1.Action1Execute(Sender: TObject);
begin
if (GetKeyState(VK_CONTROL) and $8000 = 0) then
// process normal click
else
// process ctrl click
end;
I try to combine 2 answers from author NGLN and come out with the following.
Define a new class inherit from TPopupList:
TMyPopupList = class(TPopupList)
protected
procedure WndProc(var Message: TMessage); override;
end;
procedure TMyPopupList.WndProc(var Message: TMessage);
var H: HWND;
begin
case Message.Msg of
WM_MBUTTONDOWN: begin
H := FindWindow(PChar('#32768'), nil);
SendMessage(H, WM_IME_KEYDOWN, VK_RETURN, 0);
end;
end;
inherited WndProc(Message);
end;
initialization
PopupList.Free;
PopupList := TMyPopupList.Create;
end.
The Item1Click is an OnClick event handler of TMenuItem that perform based on mouse click:
procedure TForm1.Item1Click(Sender: TObject);
begin
if (GetKeyState(VK_MBUTTON) and $80 > 0) then
Caption := 'Middle Click'
else
Caption := 'Normal Click';
end;
Note: #32768 is the default window class name for a pop-up menu, see MSDN documentation.