Jump to content

Incus J

Members
  • Content Count

    159
  • Joined

  • Last visited

Posts posted by Incus J


  1. I’d like to assign event handlers to visual components on a form at design time as usual (for example by double-clicking a popup menu item’s OnClick event, or selecting an Action via the Object Inspector), but I don’t want the IDE to create the handler in the form unit itself.  I find the form unit quickly becomes cluttered and difficult to maintain.  Instead I think I’d like the IDE to create the handler in a separate controller class in another unit.

     

    Basically I’m trying to separate UI from code, but still be able to assign the event handlers rapidly at design time.  Can't quite figure it out.  I’m happy to use Actions and ActionLists if that helps - in fact that might be preferable.

     

    In the past I’ve ended up with reams of tiny procedures in the form unit that simply hand off to a controller to perform the actual task (and quite often the controller class itself then simply passes-the-buck on to the model) :

    procedure TForm1.SomeAction(Sender: TObject);
    begin
      TController.ActuallyPerformThisAction;
    end;
    
    …elsewhere…
    
    class procedure TController.ActuallyPerformThisAction;
    begin
      TModel.WellReallyThisIsYourJob;
      UpdateTheUIToReflectModel;
    end;

    In complex apps I can create multiple controller classes to handle different aspects of the app and keep things organised, which is good.  But writing multiple handlers that simply pass-the-buck seems inefficient - I feel like my time could be better spent somehow, and my code also becomes cumbersome to navigate through:  Getting from a UI control to some code that actually does something involves several ‘Find Declaration’ steps.

     

    I’m not sure I’ve explained this well, but assuming it is possible to fathom what I’m driving at, what’s a better/good way to achieve UI/code separation while maintaining rapid development in the IDE?


  2. On 4/13/2022 at 9:23 AM, egnew said:

    I set the password using:

    document.getElementById("passwordInput1").value = 'mypass'

     

    I click the button using:

    document.querySelector("input[type=submit]").click();

    Can you post your Delphi code that you are using to execute those two lines of script?

     

    document.querySelector returns the first submit button, which might not be the one you are wanting to target (the page could potentially contain more than one form).  If the ID is known you could try targeting the button in a more specific way:

    document.getElementById("text_i18n.authframe.safr.button.openonline").click();

     

    Thought:  ExecuteScript may be asynchronous, so you may need to combine the two lines of script to execute as a single statement - I don't know your existing code, but something like:

     

    s := 'document.getElementById("passwordInput1").value = "mypass"; document.getElementById("text_i18n.authframe.safr.button.openonline").click();';

    Browser1.ExecuteScript(s);

     


  3. Well the two icons are in an ImageList assigned to the control.  OnGetImageIndex sets the desired icon like this:

    procedure TForm1.TabControl1GetImageIndex(Sender: TObject; TabIndex: Integer;
      var ImageIndex: Integer);
    begin
      if TabControl1.TabIndex=TabIndex then ImageIndex:=1 else ImageIndex:=0;
      end;

    So if the tab is currently active it gets image 1, otherwise image 0.  Which is great - it works.  But only once at start up.  I can't figure out how to assign a different icon index later.


  4. I'd like to change the icon image on a tab in a TTabControl whenever that tab becomes active.  So the icon acts a bit like a highlight for the currently selected tab.  At the moment I have an ImageList assigned to the TTabControl Images property - and this contains two icons (one for Inactive, one for Active).

     

    I can use the OnGetImageIndex event to set the appropriate image for each tab - and this works initially.  The currently active tab displays one image, and all inactive tabs display the other image.  So far so good.  Unfortunately OnGetImageIndex seems to fire once per tab only, when the TTabControl first loads up (in TTabControl.Loaded -> UpdateTabImages) - it never fires again (?) - so this mechanism doesn't allow a tab's image index to be changed later, say in response to a different tab becoming active. 

     

    Could I somehow call the protected UpdateTabImages procedure manually, in response to a different tab being clicked and becoming active?


  5. I think that's actually quite a good solution!  (Especially given what TEdgeBrowser provides for you to work with :)

     

    If I've understood it correctly it does mean that JSExec2 could complete before JSExec1 completes - if JSExec1 turns out to be the lengthier process.  But it kind of gives synchronous behaviour in the end by waiting for all results to come in.

     

    Would just need to be careful to ensure script2 doesn't depend directly on the result of script1 (?) (since they might not complete in the order they were queued).


  6. I guess I could modify things so neither class initialises at startup.  And have some separate custom startup routine that explicitly initialises each class in the order I desire.  But to make that work I'd need to give each class a non-standard class initialisation method (i.e. not called Create) for me to call.  Which seems a bit ugly.  Definitely interested to hear David's thoughts!


  7. 3 hours ago, Darian Miller said:

    You'll want to read through this thread

    Oh - yes, thank you.

     

    Some time later:  My head hurts.  This line jumped out at me though:

    Quote

    Enforcing dependencies in initialisation order is not robust. I prefer to find a different way to solve such problems. 

     

    Based on what I've just read I think I might abandon trying to get TClassA to initialise before TClassB, and find another approach where it no longer matters which order the classes get initialised in.  Yet somehow I've got to get that data storage path initialised before TClassB initialises - but since the path will be used by other classes too, I can't really do the obvious and get TClassB to initialise the path itself.


  8. Thanks - although that didn't work, it did get me thinking more closely about what is actually happening:  TClassA is a Controller that uses TClassB (Model), so B probably gets class construction priority.

     

    However although TClassB is unaware of TClassA (as it currently stands unitA uses unitB, but unitB does not use unitA) TClassA initialises a data storage path elsewhere that TClassB happens to use.  I sense a restructuring is required.


  9. ExecuteScript runs asynchronously I think - it returns (potentially) before the script has finished executing.  There's a web page View Source example here, which looks similar to your setup, but the event has different parameters:

     

    https://blogs.embarcadero.com/execute-scripts-and-view-source-with-tedgebrowser/

     

    uses
      System.NetEncoding;
    
    procedure TEdgeViewForm.EdgeBrowser1ExecuteScript(Sender: TCustomEdgeBrowser;
      AResult: HRESULT; const AResultObjectAsJson: string);
    begin
      if AResultObjectAsJson <> 'null' then
        memoHTML.Text := TNetEncoding.URL.Decode(AResultObjectAsJson).DeQuotedString('"');
    end;

    The above code looks slightly odd at a glance, in that the event is named ExecuteScript too - perhaps it's a typo?

     

    A really messy way to wait, at least just as a quick test, might be:

    procedure TForm1.Button1Click(Sender: TObject);
    var
      LMyScript: String;
    begin
      FMyResult := ''; // Clear it
      LMyScript := 'window.chrome.webview.postMessage("MyResult");';
      EdgeBrowser.ExecuteScript(LMyScript);
    
      //How wait for EdgeBrowserWebMessageReceived() to get FMyResult?
      while FMyResult = '' do Application.ProcessMessages; // Not good - but might work
    
      ShowMessage(FMyResult);
    end;

    Of course if the script fails to return a result, your app is left hanging - and Application.ProcessMessages would allow other events to trigger that you might not be anticipating - like the user clicking buttons in the UI.  Assuming synchronous behaviour is desired, you could perhaps avoid Application.ProcessMessages altogether by moving ShowMessage(FMyResult); into the WebMessageReceived handler, and disable the UI while your function executes?

     

    Incidentally I've posted a feature request for an improved FMX TWebBrowser.EvaluateJavascript function that returns a result.  I don't think FMX TWebBrowser currently surfaces an OnScriptExecutionCompleted event: https://quality.embarcadero.com/browse/RSP-35537

     

    Since getting a result back from script execution is a very common use case, I think it would be great if VCL TEdgeBrowser and FMX TWebBrowser could be enhanced to include this sort of functionality built in (synchronous and asynchronous).


  10. I have two classes (TClassA and TClassB) declared in separate units.  Both have a class constructor which executes automatically when the app starts:

     

    type TClassB=class(TObject)
      class constructor Create;
    
    ...
    
    class constructor TClassB.Create;
    begin
      //Some class initialization code
    end

    Note that this is a class constructor, not a regular instance constructor.

     

    At present, when the app starts up TClassB's class constructor runs first, and then TClassA's class constructor follows afterwards.

    In this particular case I want the opposite to occur - I want TClassA to run its class constructor code before TClassB runs its class constructor.

     

    How can I achieve that?  (I've tried swapping the order of the unit declarations in the Project source .dpr file, but that had no effect) 


  11. Thank you - I'm currently reading.

     

    Yes, I'm using Delphi's built in FMX TWebBrowser control - which has a WindowsEngine property to select the Edge engine at design time.  I just haven't figured out how to get it to work at runtime.

     

    It is interesting that the VCL TEdgeBrowser control exposes the BrowserExecutableFolder property in the Object Inspector at design time.  But FMX TWebBrowser does not.  I wonder whether that is an intended or unintended omission?


  12. I think I'm slowly beginning to get a glimmer of understanding - it's slightly more complex than I anticipated.  As well as the EdgeView2 SDK, I've also downloaded and expanded Microsoft's Fixed-Version-Runtime of WebView2, and placed the resulting folder(s) inside my app folder.  So I think I now have all the ingredients.

     

    TWebBrowser will need to know where this Fixed-Version-Runtime folder is.  How do I set a path to this in FMX TWebBrowser?  There is the BrowserExecutableFolder property which I suspect is for this purpose.  It is defined in the FMX.WebBrowser.Win unit:

    TEdgeBrowserHelper = class helper for TWinNativeWebBrowser
    ...
    property BrowserExecutableFolder: string read GetBrowserExecutableFolder write SetBrowserExecutableFolder;

    But I've yet to discover exactly how this helper attaches to TWebBrowser itself - BrowserExecutableFolder is not a property exposed in the Object Inspector.  Presumably the private field FWeb references.

     

    Do I somehow need to cast my TWebBrowser instance to a TWinNativeWebBrowser at runtime in order to set this property?  All of this seems private - the types are declared in the unit implementation section.  I must be expected to access this some other way.

     

    How do I ensure the BrowserExecutableFolder property is set to the correct path prior to TWebBrowser attempting to initialise its view?


  13. No I hadn't - now installed, thank you!

     

    After dropping WebView2Loader.dll into the app folder, the error message has changed to "Failed to create instance of Edge browser engine"

    (The DLL file is version 1.0.664.37).  That suggests the Edge engine is available though.  It is failing here:

     

    procedure TWinNativeWebBrowser.InitializeWebView;
    begin
      ...
      // Create the environment
      var hr := CreateCoreWebView2EnvironmentWithOptions(PChar(BrowserExecutableFolder), PChar(UserDataFolder), nil,
        Callback<HResult, ICoreWebView2Environment>.CreateAs<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
          CreateEnvironmentCompleted));
      if not ProcessHResult(hr) then
      begin
        FBrowserControlState := TBrowserControlState.Failed;
        DoCreateWebViewCompleted(False);
      end;
    end;

     

    BrowserExecutableFolder and UserDataFolder both appear to be empty - so I need to check whether that is significant.  

    ProcessHResult(hr) is returning False for a hr value of -2147024894.


  14. When I drag and drop new controls from the Palette onto an FMX Form, I'm finding the control tends to be created some way off the right and bottom edge of the form.  In other words the new control does not appear anywhere near the location of my mouse cursor.  I can correct it by typing coordinates into the Position property in the Object Inspector.

     

    Is anyone else experiencing this, or is it a quirk of my setup?  My system is High-DPI.


  15. I'm unsure how to get FMX TWebBrowser to use the Edge engine instead of the older IE engine.

     

    I've set its WindowsEngine property to EdgeOnly (Delphi 11).  But I'm seeing a runtime error "Edge browser engine unavailable".

     

    Edge is installed.  I think in the past a .dll file was required (?)  I'm not sure whether this is still the case.  After a Google search I looked in Delphi's Redist folder - but can't see anything that looks promising there.

     

    (On a vaguely related note - is there a way to z-order web controls in FMX?  I'd like to place some controls so that they partially overlap TWebBrowser - appear on top of the web content - but they get clipped.)


  16. Thank you Remy, Wil, f.m,

     

    The code I found online is the same fmxexpress example Wil mentions above, so I've been playing around with it.  If I comment out the following block, then it works without crashing the app:

    if ADir <> '' then begin
      LInitialDir := TNSURL.Create;
      LInitialDir.initFileURLWithPath(NSSTR(ADir));
      LOpenDir.setDirectoryURL(LInitialDir);
    end;

    The crash was occurring on LInitialDir.initFileURLWithPath(NSSTR(ADir)); and when I stepped through the code ADir appeared to be '' empty, so I'm not sure how this block was executing at all.  I guess without this block I'm losing the capability for specifying an initial starting folder.

     

    I also get a warning that NSSTR is deprecated, but the suggested replacement StrToNSStr is not recognised.  Is there an easy way to query which system unit a function might be in?


  17. Does Delphi 10.4 FMX have a built in routine or component to display a Choose a Folder dialog?  I can see dialog components to open and save a file, but not to select a folder.  If there isn't anything built in, how might I achieve this?

     

    I'm looking for something that will work on both Windows and macOS.  I have found some code online, and the Windows side of it works, but it looks fairly elaborate, and the macOS code crashes the app with Exception class 6.  So perhaps there is an easier approach using existing routines.  I'm running 10.4 Update 1 at present.


  18. TMyClassType = class of TMyClass;

    Can the metaclass type TMyClassType be passed into a function as a parameter? TMetaClass...?

    function CreateAnObjectByClassName(ClassName:string; BaseClass:TMetaClass..?):TPersistent;
    begin
      result := BaseClass(GetClass(ClassName)).Create;
    end;

    If that's possible, then I can have the caller provide the base class (the ancestor which introduced the virtual constructor) like this:

    object := CreateAnObjectByClassName('TMyClass2', TMyClassType);

    I'm not sure what type BaseClass would be - I don't think it's just a TClass.

×