Jump to content

Incus J

Members
  • Content Count

    157
  • Joined

  • Last visited

Everything 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. Thank you David, Gary, Pat for the detailed replies - there are lots of ideas for me to try out there. I do like that idea of moving other non-visual controls (e.g. popup menus) off the main form onto a data module too.
  3. Thank you David - that approach sounds very promising. I've never considered data modules before (I probably made an assumption it was something to do with databases) - so I'll go and have a play around with that idea. I'm guessing from your description that the Object Inspector will become aware of, and let me select actions I've defined in a separate data module - providing I add the data module to the main form's 'uses' section? Attila, yes you're right - I think it's that the form unit interface section is already populated with numerous entries for all the components I've added to the form. If I start adding my code or classes in this unit, it feels cluttered - but that's maybe just a perception on my part. (I know that when I came to convert an older VCL project to FMX, application code written directly in the VCL form handlers proved time consuming to extract.) I was kind of hoping for a non-visual TController organisational component that I could then assign to controls to indicate to the IDE "create your event handlers in here instead". David's suggestion above, using a separate data module and action list, sounds like it might give me something close to that. Pat, do you mean create my controller classes on a secondary form (instead of in a plain unit)? I'm not sure whether I've understood you fully - could you elaborate?
  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. Incus J

    TEdgeBrowser Script to Set Password and Click Submit

    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);
  6. Incus J

    How to change the icon of the active tab in TTabControl?

    A local class helper did the trick - perfect - thank you! Now I just call UpdateTabImages in my OnChange event handler.
  7. Incus J

    How to change the icon of the active tab in TTabControl?

    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.
  8. Incus J

    How to change the icon of the active tab in TTabControl?

    Maybe - I'm not sure how though (it's a TTabControl rather than a TPageControl - so there's no icon index property for each tab).
  9. I'd like to search for, and then highlight a line or phrase of text in a TMemo. I can get the Caret to move to the start of the correct location via SelStart, but when I set SelLength the selection does not expand visibly. The text highlight does not appear. Here's my approach : s := 'section'; i := MyMemo.Lines.Text.IndexOf('['+s+']'); if i>-1 then begin MyMemo.SetFocus; MyMemo.SelStart := i+1; MyMemo.SelLength := s.Length; MyMemo.Repaint; //MyMemo.ScrollTo(MyMemo.Caret.Pos.Y,0); I suspect the selection is actually being made successfully, internally behind the scenes, but the control is failing to highlight its current selection. If I start typing, the not-visibly-selected text is replaced with what I type. I added Repaint to see whether that would give the control a gentle kick to persuade it to display its selection highlight, but no joy so far. I can highlight text with the mouse OK, and the control is set to display its highlight whether focussed or not (HideSelectionOnExit is false). The memo is not currently ReadOnly. I've also tried toggling the control type between Platform and Styled, but the behaviour remains the same. (You can also spot that I'd eventually like to scroll the selected line into view, and I'd also like this to happen on a ReadOnly memo without giving the memo focus - but one step at a time.)
  10. I have a class descended from TInterfacedPersistent like this: type TMyClass=class(TInterfacedPersistent,IMyInterface); …and several classes descended from this, all registered with RegisterClasses([TMyClass1, TMyClass2, …etc…]); I’d like to make a very general function to create instances of these (and other classes) at runtime, by simply passing in a string containing the class name: function CreateAnObjectByClassName(AClassName:string):TPersistent; begin result := GetClass(AClassName).Create; end; This kind of works, except that when creating the object, it looks like its ancestor TObject.Create constructor is called, instead of TMyClass2.Create; When I step through the code at runtime I end up at constructor TObject.Create; in the System unit. As a result the newly created object is not initialised correctly. Having just the class name (e.g. 'TMyClass2') as a string, how can I create a genuine instance of that class, as if I had hard coded TMyClass2.Create; ?
  11. 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)
  12. Thanks Dave and Kas - some really neat approaches there - I will give them a try!
  13. Incus J

    Exec JS scripts in TEdgeBrowser

    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).
  14. 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!
  15. Oh - yes, thank you. Some time later: My head hurts. This line jumped out at me though: 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.
  16. 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.
  17. Incus J

    Exec JS scripts in TEdgeBrowser

    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).
  18. Incus J

    TWebBrowser Edge Engine Unavailable

    I think I've got it working by installing the Evergreen-Standalone version of WebView2. So it could simply be that the FMX TWebBrowser doesn't support the Fixed-Version of WebView2 currently.
  19. Incus J

    TWebBrowser Edge Engine Unavailable

    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?
  20. Incus J

    TWebBrowser Edge Engine Unavailable

    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?
  21. Incus J

    TWebBrowser Edge Engine Unavailable

    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.
  22. 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.
  23. Incus J

    Choose a Folder dialog

    FMX.Dialogs function SelectDirectory(const Caption: string; const Root: string; var Directory: string): Boolean; Thank you f.m - that's exactly what I'm looking for!
  24. Incus J

    Choose a Folder dialog

    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?
  25. 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.
×