Recent

Author Topic: Passing value between units  (Read 3760 times)

GregB

  • New Member
  • *
  • Posts: 14
Passing value between units
« on: September 22, 2015, 12:33:59 pm »
I am trying to pass the selection made by the user in the pop up menu in unit 2 back to the calling procedure in unit test

if i remove the line 
   ShowMessage('Calc .. retval = ' + IntToStr(Form2.retval));

the wrong value is returned - appears to be the previous value.

on the first pick the user picks option 1 then 0 is returned
second time user pics option 2 then 1 is returned etc

any suggestions on how to fix this or suggest a better way of doing this would be appreciated

Thanks
GregB


Code: [Select]
unit Test;

{$mode objfpc}{$H+}

interface

uses
      Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Menus,
      Unit2;

type

{ TForm1 }

      TForm1 = class(TForm)
MainMenu1: TMainMenu;
MenuItem1: TMenuItem;
MenuItem2: TMenuItem;
PopupMenu1: TPopupMenu;
procedure FormCreate(Sender: TObject);
      procedure MenuItem1Click(Sender: TObject);
      private
            { private declarations }
      public
            { public declarations }
      end;

var
      Form1: TForm1;

      x : integer;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.MenuItem1Click(Sender: TObject);
begin
  x := 0;
  Calc(x);
  ShowMessage('Get Result .. ' + IntToStr(x));
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
   PopUpMenu;
end;

end.                   


Code: [Select]
unit Unit2;

{$mode objfpc}{$H+}

interface

uses
      Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Menus;

type

{ TForm2 }

      TForm2 = class(TForm)
MenuItem1: TMenuItem;
MenuItem2: TMenuItem;
PopupMenu1: TPopupMenu;
//procedure FormCreate(Sender: TObject);
            procedure MenuItem1Click(Sender: TObject);
procedure MenuItem2Click(Sender: TObject);
procedure PopupMenu1Popup(Sender: TObject);
      private
            { private declarations }

      public
            { public declarations }
            var retval : integer;
      end;

var
      Form2: TForm2;

      //retval : integer;


Procedure Calc(var v : integer);

implementation

{$R *.lfm}

Procedure Calc(var v : integer);
begin
  Form2.PopupMenu := Form2.PopUpMenu1;                        { links popupmenu1 to the form2 }
  Form2.PopupMenu1.Items[0].Caption := 'option 1';
  Form2.PopupMenu1.Items[1].Caption := 'option 2';
  Form2.PopUpMenu1.PopUp;                                     { displays the pop up menu }
[color=red][b]  ShowMessage('Calc .. retval = ' + IntToStr(Form2.retval));  [/b][/color]{ if i remove this it does not return the correct value}
  v := Form2.retval;
end;

{ TForm2 }

//procedure TForm2.FormCreate(Sender: TObject);
//begin
//  //Form2.Show;
//end;

procedure TForm2.MenuItem1Click(Sender: TObject);
begin
  //ShowMessage('MenuItem1Click .. retval = ' + IntToStr(retval));
  retval := 1;
  //ShowMessage('MenuItem1Click .. retval = ' + IntToStr(retval));
end;

procedure TForm2.MenuItem2Click(Sender: TObject);
begin
  //ShowMessage('MenuItem2Click .. retval = ' + IntToStr(retval));
  retval := 2;
  //ShowMessage('MenuItem2Click .. retval = ' + IntToStr(retval));
end;

procedure TForm2.PopupMenu1Popup(Sender: TObject);
begin
  //ShowMessage('PopupMenu1Popup');
  PopupMenu;
end;

end.

rvk

  • Hero Member
  • *****
  • Posts: 6910
Re: Passing value between units
« Reply #1 on: September 22, 2015, 01:07:25 pm »
That's because when you execute Form2.PopUpMenu1.PopUp; control directly returns to the procedure Calc and the menu/form2 is shown in the background. Calc continues (while form2 is also shown) and shows you the wrong (previous) value.

Why don't you consider using Form2.Showmodal and returning the correct value in ModalResult. Otherwise you need to put in a while loop until you checked that the user clicked a menu-item (and using application.processmessages). But using ModalResult and Showmodal seems like a more appropriate solution here.

Edit: Yep. I was wrong. Popup does not return until you made your choice. But you should allow the program to update the value like howardpc shows. I still feel like this could be made cleaner (especially if form2 doesn't have to visible all the time) but howardpcs solution should work.

The exact reason is that the flow of your program is that the call returns from Popup-command BEFORE it reaches the TForm2.MenuItem2Click procedure. When in the Popup a menu is clicked a new event is put in the message-queue and the flow continues (back to the command after Popup). So the TForm2.MenuItem2Click is executed AFTER it returns from Popup (even after the calc-procedure ends and the TForm1.MenuItem1Click has ended). The ProcessMessages allows the program to first execute the message-queue (and thus TForm2.MenuItem2Click) before continuing.

Code: [Select]
  Form2.PopUpMenu1.PopUp;                                     { displays the pop up menu }
                                    // this returns DIRECTLY after a menuclick, even before that event is executed

  Application.ProcessMessages;      // With this it should work
                                    // It allows the menuclickevent to be executed before flow is returned to this routine

  //ShowMessage('Calc .. retval = ' + IntToStr(Form2.retval));
  v := Form2.retval;
« Last Edit: September 22, 2015, 01:46:35 pm by rvk »

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Passing value between units
« Reply #2 on: September 22, 2015, 01:11:56 pm »
You have to allow time for the value to be update and read.
A cleaner way to pass data is to provide a dedicated function.
The following two example units (test and unit2) give you the idea.
I've used a button event to call the function rather than a menu click for clarity.

Code: [Select]
unit test;

{$mode objfpc}{$H+}

interface

uses
  Forms, Dialogs, StdCtrls;

type

  { TForm1 }

  TForm1 = class(TForm)
    BGetValue: TButton;
    procedure BGetValueClick(Sender: TObject);
  end;

var
  Form1: TForm1;
  x: integer = -1;

implementation

uses unit2;

{$R *.lfm}

{ TForm1 }

procedure TForm1.BGetValueClick(Sender: TObject);
begin
  x:=GetForm2ReturnValue;
  ShowMessageFmt('GetForm2ReturnValue=%d', [x]);
end;

end. 

This is unit2:

Code: [Select]
unit Unit2;

{$mode objfpc}{$H+}

interface

uses
  Forms, Menus;

type

  { TForm2 }

  TForm2 = class(TForm)
    MenuItem1: TMenuItem;
    MenuItem2: TMenuItem;
    PopupMenu1: TPopupMenu;
    procedure MenuItem1Click(Sender: TObject);
    procedure MenuItem2Click(Sender: TObject);
  private
    FReturnValue: integer;
  public
    property ReturnValue: integer read FReturnValue;
  end;

  function GetForm2ReturnValue: integer;

var
  Form2: TForm2;

implementation

function GetForm2ReturnValue: integer;
begin
  Form2.PopupMenu1.PopUp;
  Application.ProcessMessages;
  Result:=Form2.ReturnValue;
end;

{$R *.lfm}

{ TForm2 }

procedure TForm2.MenuItem1Click(Sender: TObject);
begin
  FReturnValue:=1;
end;

procedure TForm2.MenuItem2Click(Sender: TObject);
begin
  FReturnValue:=2;
end;

end.

GregB

  • New Member
  • *
  • Posts: 14
Re: Passing value between units
« Reply #3 on: September 22, 2015, 02:03:17 pm »
Thanks guys ... appreciate your help immensely.    I am trying to figure these things out myself but this had me stumped.

I modified unit2 as shown below and it now works.   Is this acceptable or bad programming?

I will have to study the code you posted

Code: [Select]
unit Unit2;

{$mode objfpc}{$H+}

interface

uses
      Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Menus;

type

{ TForm2 }

      TForm2 = class(TForm)
MenuItem1: TMenuItem;
MenuItem2: TMenuItem;
PopupMenu1: TPopupMenu;
procedure MenuItem1Click(Sender: TObject);
procedure MenuItem2Click(Sender: TObject);
procedure PopupMenu1Popup(Sender: TObject);
      private
            { private declarations }

      public
            { public declarations }
      end;

var
      Form2: TForm2;


Procedure Calc(var v : integer);

implementation

{$R *.lfm}

Procedure Calc(var v : integer);
begin
  Form2.PopupMenu := Form2.PopUpMenu1;                        { links popupmenu1 to the form2 }
  Form2.PopupMenu1.Items[0].Caption := 'option 1';
  Form2.PopupMenu1.Items[1].Caption := 'option 2';
  Form2.PopUpMenu1.PopUp;                                      { displays the pop up menu }

  Form2.Showmodal;                  // added

  v := Form2.ModalResult;         // added
end;

{ TForm2 }


procedure TForm2.MenuItem1Click(Sender: TObject);
begin
  modalresult := 1;    //** changed
end;

procedure TForm2.MenuItem2Click(Sender: TObject);
begin
 modalresult := 2;    //** changed
end;

procedure TForm2.PopupMenu1Popup(Sender: TObject);
begin
  PopupMenu;
end;

end.   

rvk

  • Hero Member
  • *****
  • Posts: 6910
Re: Passing value between units
« Reply #4 on: September 22, 2015, 02:13:12 pm »
Showmodal is a function which returns the ModelResult already by itself.
So this would be sufficient:
Code: [Select]
  v := Form2.ModalResult;         // added

But... you're still calling Form2.PopUpMenu1.PopUp; directly. In that case you shouldn't use ShowModal. Either use Showmodal and show the Popup in the OnShow of Form2 or use the Form2.PopUpMenu1.PopUp with Processmessages. Don't use both.

But why are you using a different form for the Popup if you don't show Form2 at all?? Is there something on Form2 which you need. Otherwise I would just make a second PopupMenu on Form1 which you can use.


I would also take care of those indents.
If you install the jcfidelazarus package you can press Ctrl+D and have the code-formatter take care of it all. It will produce much cleaner source-code.


GregB

  • New Member
  • *
  • Posts: 14
Re: Passing value between units
« Reply #5 on: September 22, 2015, 03:09:56 pm »
There is nothing on Form2 that i need, just thought you needed a form to attach the pop up to.

The Procedure that i want to use this in is one i use fairly often and want it to be a standalone Procedure .. with no need for anything other than the procedure cal.   It produces two results so the user must select whichever one they require.

Have taken care of the formatting with your suggestion.

I have not done a lot of programming since using Turbo Pascal 5 (dos) and have difficulty getting my head around the program flow ... really need to find a good book for this.

Thanks again, really appreciate your help. 

end result

Code: [Select]
unit Unit2;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Menus;

type

  { TForm2 }

  TForm2 = class(TForm)
    MenuItem1: TMenuItem;
    MenuItem2: TMenuItem;
    PopupMenu1: TPopupMenu;
    procedure MenuItem1Click(Sender: TObject);
    procedure MenuItem2Click(Sender: TObject);
    procedure PopupMenu1Popup(Sender: TObject);
  private
    { private declarations }

  public
    { public declarations }
  end;

var
  Form2: TForm2;


procedure Calc(var v: integer);

implementation

{$R *.lfm}

procedure Calc(var v: integer);
begin
  { links popupmenu1 to the form2 }
  Form2.PopupMenu := Form2.PopUpMenu1;
  Form2.PopupMenu1.Items[0].Caption := 'option 1';
  Form2.PopupMenu1.Items[1].Caption := 'option 2';
  { displays the pop up menu }
  Form2.PopUpMenu1.PopUp;

  {http://forum.lazarus.freepascal.org/index.php/topic,29749.msg188277.html#new}
  Application.ProcessMessages;      // With this it should work
                                    // It allows the menuclickevent to be executed before flow is returned to this routine
  //Form2.Showmodal;

  v := Form2.ModalResult;
end;

{ TForm2 }


procedure TForm2.MenuItem1Click(Sender: TObject);
begin
  //ShowMessage('MenuItem1Click .. retval = ' + IntToStr(retval));
  modalresult := 1;
  //ShowMessage('MenuItem1Click .. retval = ' + IntToStr(retval));
end;

procedure TForm2.MenuItem2Click(Sender: TObject);
begin
  //ShowMessage('MenuItem2Click .. retval = ' + IntToStr(retval));
  modalresult := 2;
  //ShowMessage('MenuItem2Click .. retval = ' + IntToStr(retval));
end;

procedure TForm2.PopupMenu1Popup(Sender: TObject);
begin
  //ShowMessage('PopupMenu1Popup');
  PopupMenu;
end;

end.

rvk

  • Hero Member
  • *****
  • Posts: 6910
Re: Passing value between units
« Reply #6 on: September 22, 2015, 03:26:13 pm »
You still have have both ModalResult-method and the Form2.PopUpMenu1.PopUp; in your procedure. You need to use one or the other. Not both. As you have it now you first let the user choose (with Form2.PopUpMenu1.PopUp) and then show the Form2 which immediately closes because ModalResult is set. But your form2 will flicker very shortly. If you don't want form2 to show you just need to place your retval-value lines back in the MenuItemClicks and remove the Showmodal, in which case there is no more flickering. You only use the Showmodal-method if there is something on form2 which you want to show (which in your example there is not).
Code: [Select]
  Form2.PopUpMenu1.PopUp;

  {http://forum.lazarus.freepascal.org/index.php/topic,29749.msg188277.html#new}
  Application.ProcessMessages;      // With this it should work
                                    // It allows the menuclickevent to be executed before flow is returned to this routine
  v := retval;

  //Form2.Showmodal;
  // v := Form2.ModalResult;      // <-- not needed because you already use PopUpMenu1.PopUp;
                                  // only do Showmodal when there is something visible on form2 you need
end;

procedure TForm2.MenuItem1Click(Sender: TObject);
begin
  //ShowMessage('MenuItem1Click .. retval = ' + IntToStr(retval));
  retval := 1;
  //ShowMessage('MenuItem1Click .. retval = ' + IntToStr(retval));
end;

Yes. You can have lots of PopupMenus on one form. For example if you put your PopupMenu from Form2 on Form1 (with all the events and naming it MyPopupMenu2) you can just do MyPopupMenu2.Popup on Form1 to get the value. You can also call Form1.MyPopupMenu2.Popup with ProcessMessages to get the value from Form1 (or from a procedure like you did now).

But if you have lots of things you want to do/code in the MenuItemClicks and you want the logic/code in a separate unit, then using a "dummy"-form is a possible way to do it.

Also... I don't think you need the TForm2.PopupMenu1Popup() at all. If you call Form2.PopUpMenu1.PopUp directly (which you did) you don't need to call the default popupmenu attached to form2 (if there is any).

(PopupMenu in TForm2.PopupMenu1Popup() calls the popupmenu you assigned to Form2. If you didn't assign anything there nothing happens. If you assigned PopupMenu1 from Form2 then you essentially get 2 Popup events. The first one from directly calling Form2.PopUpMenu1.PopUp which in turn calls TForm2.PopupMenu1Popup() which again calls the default Popup-event of Form2.PopupMenu. You probably don't see it but the TForm2.PopupMenu1Popup() is completely not needed there)


end result would be:
Code: [Select]
unit Unit2;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Menus;

type

  { TForm2 }

  TForm2 = class(TForm)
    MenuItem1: TMenuItem;
    MenuItem2: TMenuItem;
    PopupMenu1: TPopupMenu;
    procedure MenuItem1Click(Sender: TObject);
    procedure MenuItem2Click(Sender: TObject);
  private
    { private declarations }

  public
    { public declarations }
    retval: integer; // no var needed in front of this !
  end;

var
  Form2: TForm2;

procedure Calc(var v: integer);

implementation

{$R *.lfm}

procedure Calc(var v: integer);
begin
  { links popupmenu1 to the form2 }
  Form2.PopupMenu := Form2.PopUpMenu1;
  Form2.PopupMenu1.Items[0].Caption := 'option 1';
  Form2.PopupMenu1.Items[1].Caption := 'option 2';
  { displays the pop up menu }
  Form2.PopUpMenu1.PopUp;
  {http://forum.lazarus.freepascal.org/index.php/topic,29749.msg188277.html#new}
  Application.ProcessMessages;      // With this it should work
  v := Form2.retval;
end;

{ TForm2 }

procedure TForm2.MenuItem1Click(Sender: TObject);
begin
  retval := 1;
end;

procedure TForm2.MenuItem2Click(Sender: TObject);
begin
  retval := 2;
end;

end.
« Last Edit: September 22, 2015, 03:34:51 pm by rvk »

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: Passing value between units
« Reply #7 on: September 22, 2015, 05:00:15 pm »
There is nothing on Form2 that i need, just thought you needed a form to attach the pop up to.
The Procedure that i want to use this in is one i use fairly often and want it to be a standalone Procedure .. with no need for anything other than the procedure cal.   It produces two results so the user must select whichever one they require.

I think you are looking for a function something like the following. Adapt the code to your needs. You can generate it by dropping a single button on the main form of a new Lazarus project and completing the unit as follows:

Code: [Select]
unit test;

{$mode objfpc}{$H+}

interface

uses
  Forms, Dialogs, StdCtrls, menus, sysutils, Classes;

type

  { TForm1 }

  TForm1 = class(TForm)
    BGetValue: TButton;
    procedure BGetValueClick(Sender: TObject);
  private
    procedure OnPopupClick(Sender: TObject);
  end;

  function GetOptionValue(const numberOfOptions: integer; onClickEvent: TNotifyEvent): integer;

var
  Form1: TForm1;

implementation

var
  popupReturnValue: integer = -1;
  pop: TPopupMenu = nil;

function GetOptionValue(const numberOfOptions: integer; onClickEvent: TNotifyEvent): integer;
var
  i: integer;
  mi: TMenuItem;

  function NewPopupItem(anID: integer): TMenuItem;
  begin
    Result:=TMenuItem.Create(Application);
    Result.Caption:=Format('Choose option %d',[anID]);
    Result.Tag:=anID;
    Result.OnClick:=onClickEvent;
  end;

begin
  if pop = nil then
    begin
      pop:=TPopupMenu.Create(Application);
      for i:=0 to numberOfOptions-1 do
        begin
          mi:=NewPopupItem(i+1);
          pop.Items.insert(i, mi);
        end;
    end;
  pop.PopUp;
  Application.ProcessMessages;
  Result:=popupReturnValue;
  popupReturnValue:= -1;
end;

{$R *.lfm}

{ TForm1 }

procedure TForm1.BGetValueClick(Sender: TObject);
var
  x, optionCount: integer;
begin
  optionCount:=3; // use 1, 2, 3 or more...
  x:=GetOptionValue(optionCount, @Form1.OnPopupClick);
  ShowMessageFmt('Of the %d options you chose option %d', [optionCount, x]);
end;

procedure TForm1.OnPopupClick(Sender: TObject);
var
  mi: TMenuItem absolute Sender;
begin
  if not (Sender is TMenuItem) then
    Exit;
  popupReturnValue:=mi.Tag;
end;

end.
« Last Edit: September 22, 2015, 05:39:09 pm by howardpc »

 

TinyPortal © 2005-2018