AfxNova progress

Started by José Roca, November 14, 2025, 11:37:56 PM

Previous topic - Next topic

José Roca

Bad news. It is not possible to implement synchronous Get‑functions with WebView2. Any waiting mechanism—no matter how it is written—prevents the asynchronous callback from firing. This is not a bug: it is a design limitation of WebView2, which does not allow reentrancy on the UI thread.

As a consequence, the only reliable way to obtain information (for example, the position of a shape) is to request it and handle the result inside the asynchronous callback. It cannot be returned directly as the result of a Get‑function.

Paul Squires

So I guess that the lesson learned is that all layout work and position manipulation needs to be done in the frontend using javascript. If something in the frontend is important enough that the backend needs to know about, then javascript could send some type of notification to the freebasic code on the backend (rather than freebasic doing all the requesting). Like, if the application was shutting down, javascript could catch that event, get the window dimensions from the DOM and then send a notification message to some type of freebasic handler with the coordinates to be saved to a config file.
Paul Squires
PlanetSquires Software

José Roca

I have written procedures for all the primitives and also a generic function for drawing any kind of shape y you provide the javascript code.

For example, this procedure draws a rectangle:

' ========================================================================================
' Draws a rectangle on the Konva canvas.
' Parameters:
'   id          : Optional identifier for the rectangle (case-sensitive)
'   x           : X coordinate of the top-left corner (in pixels)
'   y           : Y coordinate of the top-left corner (in pixels)
'   w           : Width of the rectangle (in pixels)
'   h           : Height of the rectangle (in pixels)
'   fillColor   : Fill color of the rectangle (HTML/CSS color string)
'   strokeColor : Border color of the rectangle (HTML/CSS color string)
'   strokeWidth : Width of the border line (in pixels)
'   draggable   : If TRUE, the rectangle can be dragged with the mouse
' Description:
'   Creates a new Konva.Rect object and adds it to the main layer.
'   If an id is provided, it is assigned to the shape so it can be
'   modified later (e.g., AddShadow, MoveTo, etc.)
' Notes:
'   - If draggable is TRUE, the cursor changes to "move" when hovering or dragging.
'   - The shape is stored in window.konvaShapes[id] if an id is provided.
' Usage example:
'   pKonva->DrawRectangle("rect1", 100, 80, 400, 200, "orange", "black", 2, TRUE)
' ========================================================================================
PRIVATE SUB CKonva.DrawRectangle (BYREF id AS STRING = "", BYVAL x AS LONG, BYVAL y AS LONG, _
   BYVAL w AS LONG, BYVAL h AS LONG, BYREF fillColor AS STRING = "transparent", _
   BYREF strokeColor AS STRING = "black", BYVAL strokeWidth AS LONG = 2, _
   BYVAL draggable AS BOOLEAN = FALSE)

   DIM idPart AS STRING
   IF id <> "" THEN idPart = "id:'" & id & "',"

   DIM js AS STRING
   js = _
      "var rect = new Konva.Rect({" _
      & idPart _
      & "x:" & STR(x) & "," _
      & "y:" & STR(y) & "," _
      & "width:" & STR(w) & "," _
      & "height:" & STR(h) & "," _
      & "fill:'" & fillColor & "'," _
      & "stroke:'" & strokeColor & "'," _
      & "strokeWidth:" & STR(strokeWidth) & "," _
      & "draggable:" & IIF(draggable, "true", "false") _
      & "});" _
      _
      "layer.add(rect);" _
      _
      "if ('" & id & "' !== ''){" _
      & "   if (!window.konvaShapes) window.konvaShapes = {};" _
      & "   window.konvaShapes['" & id & "'] = rect;" _
      & "}" _
      _
      "if (" & IIF(draggable, "true", "false") & "){" _
      & "   rect.on('mouseover', function(){ document.body.style.cursor = 'move'; });" _
      & "   rect.on('mouseout', function(){ document.body.style.cursor = 'default'; });" _
      & "   rect.on('dragstart', function(){ document.body.style.cursor = 'move'; });" _
      & "   rect.on('dragend', function(){ document.body.style.cursor = 'move'; });" _
      & "}" _
      _
      "layer.draw();"

   this.ExecuteScript(js, NULL)

END SUB
' ========================================================================================

This way, you can draw a rectangle without needing to know anything about javascript.

There are also procedures that can be used with any shape, for example this one to change the position:

' ========================================================================================
' * Moves a shape identified by its ID to a new (x, y) position.
' Parameters:
'   id        : Identifier of the shape (case-sensitive)
'   newX      : New X coordinate
'   newY      : New Y coordinate
'   duration  : Animation duration in seconds (0 = no animation)
' Description:
'   Finds the shape by ID and updates its position.
'   If the ID does not exist, nothing happens.
' Usage example:
'   pKonva->MoveTo("rect1", 200, 150, 1.5)
' ========================================================================================
PRIVATE SUB CKonva.MoveTo (BYREF id AS STRING, BYVAL newX AS LONG, BYVAL newY AS LONG, BYVAL duration AS DOUBLE = 0)
   IF duration <= 0 THEN
      ' Direct movement
      DIM js AS STRING = _
         "var s = layer.findOne('#" & id & "');" _
         & "if(s){" _
         & "  s.x(" & STR(newX) & ");" _
         & "  s.y(" & STR(newY) & ");" _
         & "  layer.draw();" _
         & "}"
      this.WaitFor("layer.findOne('#" & id & "')", js, 50, 5000)
   ELSE
      ' Animated movement
      DIM props AS STRING
      props = "x=" & STR(newX) & "; y=" & STR(newY) & "; duration=" & STR(duration)
      this.Tween(id, props)
   END IF
END SUB
' ========================================================================================

Other procedures of general use:

   ' --- Methods of general use
   DECLARE SUB ClearCanvas (BYREF colour AS STRING = "transparent")
   DECLARE SUB WaitFor (BYREF conditionJS AS STRING, BYREF actionJS AS STRING, BYVAL intervalMS AS LONG = 50, BYVAL timeoutMS AS LONG = 5000)
   DECLARE SUB AddGlow (BYREF id AS STRING, BYREF colour AS STRING, BYVAL blur AS DOUBLE, BYVAL opacity AS DOUBLE)
   DECLARE SUB AddLinearGradient (BYREF id AS STRING, BYVAL x0 AS DOUBLE, BYVAL y0 AS DOUBLE, _
           BYVAL x1 AS DOUBLE, BYVAL y1 AS DOUBLE, BYREF stops AS STRING)
   DECLARE SUB AddRadialGradient (BYREF id AS STRING, BYVAL cx AS DOUBLE, BYVAL cy AS DOUBLE, _
           BYVAL r0 AS DOUBLE, BYVAL r1 AS DOUBLE, BYREF stops AS STRING)
   DECLARE SUB AddShadow (BYREF id AS STRING, BYREF colour AS STRING, BYVAL blur AS LONG, BYVAL offsetX AS LONG, _
           BYVAL offsetY AS LONG, BYVAL opacity AS DOUBLE)
   DECLARE SUB ClearShadow (BYREF id AS STRING)
   DECLARE SUB MoveTo (BYREF id AS STRING, BYVAL newX AS LONG, BYVAL newY AS LONG, BYVAL duration AS DOUBLE = 0)
   DECLARE SUB MoveToTop (BYREF id AS STRING)
   DECLARE SUB MoveToBottom (BYREF id AS STRING)
   DECLARE SUB Remove (BYREF id AS STRING, BYVAL duration AS DOUBLE = 0)
   DECLARE SUB Resize (BYREF id AS STRING, BYVAL newW AS LONG, BYVAL newH AS LONG, BYVAL duration AS DOUBLE = 0)
   DECLARE SUB Rotate (BYREF id AS STRING, BYVAL angle AS DOUBLE, BYREF unit AS STRING = "deg", BYVAL duration AS DOUBLE = 0)
   DECLARE SUB Scale (BYREF id AS STRING, BYVAL sx AS DOUBLE = 1.0, BYVAL sy AS DOUBLE = 1.0, BYVAL duration AS DOUBLE = 0)
   DECLARE SUB Skew (BYREF id AS STRING, BYVAL kx AS DOUBLE = 0.0, BYVAL ky AS DOUBLE = 0.0, BYVAL duration AS DOUBLE = 0)
   DECLARE SUB SetCornerRadius (BYREF id AS STRING, BYREF radius AS STRING)
   DECLARE SUB SetDash (BYREF id AS STRING, BYREF pattern AS STRING)
   DECLARE SUB SetDraggable (BYREF id AS STRING, BYVAL draggable AS BOOLEAN)
   DECLARE SUB SetFillColor (BYREF id AS STRING, BYREF colour AS STRING)
   DECLARE SUB SetLineCap (BYREF id AS STRING, BYREF capStyle AS STRING)
   DECLARE SUB SetLineJoin (BYREF id AS STRING, BYREF joinStyle AS STRING)
   DECLARE SUB SetOpacity (BYREF id AS STRING, BYVAL value AS DOUBLE, BYVAL duration AS DOUBLE = 0)
   DECLARE SUB SetStroke (BYREF id AS STRING, BYREF colour AS STRING)
   DECLARE SUB SetStrokeColor (BYREF id AS STRING, BYREF colour AS STRING)
   DECLARE SUB SetStrokeWidth (BYREF id AS STRING, BYVAL width AS LONG)
   DECLARE SUB SetVisible (BYREF id AS STRING, BYVAL isVisible AS BOOLEAN)
   DECLARE SUB SetZIndex (BYREF id AS STRING, BYVAL zIndex AS LONG)
   DECLARE SUB AddTransformer (BYREF shapeId AS STRING)
   DECLARE SUB RemoveTransformers ()

They work asynchronously, but as they don't return a result, we don't need to wait for one.

Therefore, we can do:

m_pCanvas->DrawText("text1", 130, 10, "Rectangle", 34, "Arial", "red", "transparent", 0, TRUE)
m_pCanvas->DrawRoundedRectangle("rect1", 120, 50, 200, 100, 10, "orange", "black", 2, TRUE)
m_pCanvas->DrawText("text2", 400, 10, "Circle", 34, "Arial", "red", "transparent", 0, TRUE)
m_pCanvas->AddButtonElement("btnLoad", 10, 19, "Load", "black", "Arial", 16)
m_pCanvas->AddButtonElement("btnSave", 10, 50, "Save", "black", "Arial", 16)
m_pCanvas->SetElementShadow("btnSave", "2px 2px 4px #000")
m_pCanvas->SetElementShadow("rect1'", "2px 2px 4px #000")
m_pCanvas->SetElementOutline("btnLoad", "2px dashed green")   ' Emphasize button
m_pCanvas->DrawCircle("circle1", 450, 110, 60, "", "red", 3, TRUE)
m_pCanvas->AddShadow("rect1", "black", 15, 5, 5, 0.4)
m_pCanvas->AddRadialGradient("circle1", 0, 0, 10, 120, "0, 'yellow', 1, 'red'")
m_pCanvas->DrawStar("star1", 450, 280, 5, 40, 80, "lightblue", "blue", 3, TRUE)
m_pCanvas->AddShadow("star1", "black", 5, 2, 2, 0.4)
m_pCanvas->AddShadow("circle1", "black", 10, 3, 3, 0.4)
m_pCanvas->DrawImage("img1", 120, 200, 200, 150, AfxGetExePath & "\" & "PIC_001.jpg")
m_pCanvas->AddShadow("img1", "black", 6, 4, 4, 0.4)
DIM svgData AS DWSTRING = "M10 10 L100 10 L100 100 Z"
m_pCanvas->DrawPath("triangle1", 0, 250, svgData, "yellow", "black", 3, TRUE)
m_pCanvas->AddShadow("triangle1", "black", 10, 3, 3, 0.4)

José Roca

For Get functions, I have tried:

' =====================================================================================
' Waits until the Invoke function of ICoreWebView2ExecuteScriptWithResultCompletedHandler
' interface, fired by ExecuteScriptWithResult has been completed.
' =====================================================================================
PRIVATE FUNCTION CWebView2.WaitForScriptCompletion (BYVAL hEvent AS HANDLE, BYVAL timeoutMS AS DWORD = 0) AS BOOLEAN
   DIM index AS DWORD
   DIM hr AS HRESULT
   hr = CoWaitForMultipleHandles ( _
            0, _              ' flags
            timeoutMS, _      ' timeout
            1, _              ' number of handles
            @hEvent, _        ' array of handles
            @index )          ' index of the event that caused the function to return.
   RETURN (hr = S_OK)
END FUNCTION
' =====================================================================================

' ========================================================================================
' Gets the value of the X coordinate of the specified shape.
' Parameter:
'   id : Identifier of the shape (case-sensitive)
' Usage example:
'   DIM x AS DOUBLE = pKonva.GetX("shapeId)
' ========================================================================================
PRIVATE FUNCTION CKonva.GetX (BYREF id AS STRING) AS DOUBLE

   DIM dblRes AS DOUBLE

   DO  ' // Fake loop to avoid nested IFs
      ' // Store the function name and the shape identifier
      m_pExecuteScriptRes->SetId("GetX",  id)
      ' // Create event
      m_pExecuteScriptRes->m_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL)
      ' // Execute script
      DIM js AS STRING = "stage.findOne('#" & id & "').x();"
      DIM hr AS HRESULT = this.ExecuteScriptWithResult(js, CAST(ANY PTR, m_pExecuteScriptRes))
      IF FAILED(hr) THEN EXIT DO
      ' // Wait for script to complete
      IF this.WaitForScriptCompletion(m_pExecuteScriptRes->m_hEvent) = FALSE THEN EXIT DO
      dblRes = VAL(m_pExecuteScriptRes->m_dwsJSonResult)
      EXIT DO
   LOOP

   ' // Cleanup
   IF m_pExecuteScriptRes->m_hEvent THEN CloseHandle(m_pExecuteScriptRes->m_hEvent)
   RETURN dblRes

END FUNCTION
' ========================================================================================

And in an internal implementation of the ICoreWebView2ExecuteScriptWithResultCompletedHandler interface:

' ########################################################################################
' CWebView2ExecuteScriptWithResultCompletedHandlerInternal class
' Implementation of the ICoreWebView2ExecuteScriptWithResultCompletedHandler callback interface.
' ########################################################################################
TYPE CWebView2ExecuteScriptWithResultCompletedHandlerInternal EXTENDS CWebView2ExecuteScriptWithResultCompletedHandler

   DECLARE VIRTUAL FUNCTION Invoke (BYVAL errorCode AS HRESULT, BYVAL result AS Afx_ICoreWebView2ExecuteScriptResult PTR) AS HRESULT
   DECLARE VIRTUAL DESTRUCTOR
   DECLARE SUB SetId (BYREF func AS STRING, BYREF id AS STRING)

   m_hEvent AS HANDLE
   m_Succeeded AS BOOL = 0
   m_dwsResult AS DWSTRING = ""
   m_dwsJSonResult AS DWSTRING = ""
   m_Function AS STRING = ""
   m_id AS STRING = ""

END TYPE
' ########################################################################################

' =====================================================================================
PRIVATE DESTRUCTOR CWebView2ExecuteScriptWithResultCompletedHandlerInternal
   IF m_hEvent THEN CloseHandle(m_hEvent)
END DESTRUCTOR
' =====================================================================================

' =====================================================================================
PRIVATE SUB CWebView2ExecuteScriptWithResultCompletedHandlerInternal.SetId (BYREF func AS STRING, BYREF id AS STRING)
   m_Function = func
   m_id= id
END SUB
' =====================================================================================

' =====================================================================================
PRIVATE FUNCTION CWebView2ExecuteScriptWithResultCompletedHandlerInternal.Invoke (BYVAL errorCode AS HRESULT, BYVAL result AS Afx_ICoreWebView2ExecuteScriptResult PTR) AS HRESULT
   CWV2_DP(WSTR(@this))
print "invoke"

   m_Succeeded = 0
   m_dwsResult = ""
   m_dwsJSonResult = ""
   DIM hr AS HRESULT

   IF errorCode <> S_OK THEN
      SetEvent(m_hEvent)
      RETURN S_OK
   END IF

   result->get_Succeeded(@m_Succeeded)
   IF m_Succeeded = 0 THEN
      SetEvent(m_hEvent)
      RETURN S_OK
   END IF

   DIM pwszJSonResult AS LPWSTR
   hr = Result->get_ResultAsJson(@pwszJSonResult)
   IF pwszJSonResult THEN
      m_dwsJSonResult = *pwszJSonResult
      CoTaskMemFree(pwszJSonResult)
   END IF
   DIM pwszResult AS LPWSTR
   DIM value AS BOOL
   hr = Result->TryGetResultAsString(@pwszResult, @value)
   IF pwszResult THEN
      IF value THEN m_dwsResult = *pwszResult
      CoTaskMemFree(pwszResult)
   END IF

print m_Function
print m_id
print m_dwsJSonResult; " ************"
print m_dwsResult; " ++++++++++++"

   ' // Signal than Invoke has finished
   SetEvent(m_hEvent)
print "m_hEvent", m_hEvent
   RETURN S_OK

END FUNCTION
' =====================================================================================

The problem is that if you use a wainting function, the Invoke event does not fire. It is not that WaitForScriptCompletion is wrong. It does not work no matter what you use, e.g. a loop using PEEKMESSAGE, MsgWaitForMultipleObjects or whatever. These waiting functions can cause reentrancy if, for example, the user clicks a shape that fires another event. WebView2 does not allow reentrancy and have prevented it by no firing the event until the waiting function ends.

José Roca

#64
All I can do is to write procedures like this:

PRIVATE SUB CKonva.GetX (BYREF id AS STRING)

   ' // Store the function name and the shape identifier
   m_pExecuteScriptRes->SetId("GetX",  id)
   ' // Execute script
   DIM js AS STRING = "stage.findOne('#" & id & "').x();"
   DIM hr AS HRESULT = this.ExecuteScriptWithResult(js, CAST(ANY PTR, m_pExecuteScriptRes))

END SUB

And then in the Invoke method forwards the retrieved information sending a message to the window procedure of the top-level window with a pointer to an structure with members for the name of the procedure that has fired the event, the identifier of the shape and the result as a JSon string.

This works, but does not allow linear synchronously programming.

José Roca

Instead of using Konva, I'm going to write another class using HTML5, CSS, SVG and some JavaScript and JSon.