FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin for Harbour/xHarbour MDI Child Timer conflict
Posts: 7335
Joined: Thu Oct 18, 2012 07:17 PM

MDI Child Timer conflict

Posted: Mon May 18, 2026 09:41 PM

STOP button fails to deactivate timer when second MDI window with XBROWSE refresh timer is active

Hi everyone,

I am experiencing a strange issue with TTimer and event handling in a FiveWin / Harbour MDI application.

I am developing a custom data-plotting application where the main window (oWndOsc) creates a timer to continuously push data points and animate a custom graphical object (oOsc).

Everything works perfectly on its own: when I press the START button, the data updates and the graph moves. When I press the STOP button, the timer is successfully deactivated/ended, and the graph freezes.

The button is defined like this:

DEFINE BUTTON OF oBar NOBORDER ;
       RESOURCE "Stop" ;
       PROMPT "STOP" ;
       ACTION If( oTimer != NIL, ( oTimer:Deactivate(), oTimer:End(), oTimer := NIL ), NIL )

The Problem:
From this main window, I can open a second MDI Child window (oWndDataInfo) to display the numerical sample data inside an XBROWSE. This second window has its own independent timer (oTimerInfo) running every 300ms to fetch data from my custom object and refresh the browse:

DEFINE TIMER oTimerInfo ;
       INTERVAL 300 ;
       OF oWndDataInfo ;
       ACTION UpdateSamples( oBrw, oOsc, nChannel, nStepTime )
ACTIVATE TIMER oTimerInfo

...

FUNCTION UpdateSamples( oBrw, oOsc, nMode, nT )
   local aSamples := OscGetSamplesEx( oOsc, nMode, nT )
   oBrw:SetArray( aSamples )
   oBrw:Refresh()
RETURN NIL

As soon as this second window (oWndDataInfo) is active and its timer starts running, the STOP button on the first window stops working.

When I click STOP, the action completely fails to stop the main timer (oTimer). It seems like the continuous data fetching and oBrw:Refresh() every 300ms is flooding the Windows message queue or altering the focus/execution loop, preventing the first window's button action from properly destroying or deactivating its own oTimer.

Even if I use a module-level STATIC variable for oTimer or try to pass it by reference (@oTimer), the button click appears to be completely ignored or bypassed while the XBROWSE timer is ticking. If I close the data window, the STOP button instantly starts working normally again.

Has anyone encountered this type of event loop / timer collision between MDI windows in FiveWin? What is the best way to ensure the STOP button retains priority and can successfully stop the main execution timer even while the second window is aggressively refreshing its browse?

Thanks in advance for your help!

Since from 1991/1992 ( fw for clipper Rel. 14.4 - Momos)

I use : FiveWin for Harbour March-April 2024 - Harbour 3.2.0dev (harbour_bcc770_32_20240309) - Bcc7.70 - xMate ver. 1.15.3 - PellesC - mail: silvio[dot]falconi[at]gmail[dot]com
Posts: 44229
Joined: Thu Oct 06, 2005 05:47 PM

Re: MDI Child Timer conflict

Posted: Tue May 19, 2026 03:55 AM

Dear Silvio,

Please try it this way:

#include "FiveWin.ch"

//----------------------------------------------------------------------------//
// Test 2: REAL MDI frame + 2 MDI CHILD windows, one timer each.
// Confirms TTimer dispatch/kill is MDI-independent.
//----------------------------------------------------------------------------//

function Main()

   local oFrame, oChild1, oChild2
   local oTimer1, oTimer2
   local nFired1 := 0, nFired2 := 0
   local cLog := ""

   DEFINE WINDOW oFrame MDI TITLE "MDI Frame"

   DEFINE WINDOW oChild1 MDICHILD OF oFrame TITLE "Child 1 (Osc)"
   DEFINE WINDOW oChild2 MDICHILD OF oFrame TITLE "Child 2 (DataInfo)"

   DEFINE TIMER oTimer1 INTERVAL 1000 OF oChild1 ACTION ( nFired1++ )
   DEFINE TIMER oTimer2 INTERVAL  300 OF oChild2 ACTION ( nFired2++ )

   ACTIVATE TIMER oTimer1
   ACTIVATE TIMER oTimer2

   cLog += "child1 hWnd   = " + cValToChar( oChild1:hWnd ) + CRLF
   cLog += "child2 hWnd   = " + cValToChar( oChild2:hWnd ) + CRLF
   cLog += "oTimer1:nId   = " + cValToChar( oTimer1:nId ) + CRLF
   cLog += "oTimer2:nId   = " + cValToChar( oTimer2:nId ) + CRLF
   cLog += "IDs unique    = " + cValToChar( oTimer1:nId != oTimer2:nId ) + CRLF
   cLog += "t1 hWndOwner  = " + cValToChar( oTimer1:hWndOwner ) + CRLF
   cLog += "t2 hWndOwner  = " + cValToChar( oTimer2:hWndOwner ) + CRLF
   cLog += "owner=child1  = " + cValToChar( oTimer1:hWndOwner == oChild1:hWnd ) + CRLF
   cLog += "owner=child2  = " + cValToChar( oTimer2:hWndOwner == oChild2:hWnd ) + CRLF
   cLog += "t1 active     = " + cValToChar( oTimer1:lActive ) + CRLF
   cLog += "t2 active     = " + cValToChar( oTimer2:lActive ) + CRLF

   // simulate WM_TIMER routed to each MDI child
   oChild2:Timer( oTimer2:nId )
   cLog += "child2:Timer(t2): fired1=" + cValToChar( nFired1 ) + ;
           " fired2=" + cValToChar( nFired2 ) + CRLF

   oChild1:Timer( oTimer1:nId )
   cLog += "child1:Timer(t1): fired1=" + cValToChar( nFired1 ) + ;
           " fired2=" + cValToChar( nFired2 ) + CRLF

   // STOP button on Child 1, while Child 2 timer still ticking
   oTimer1:Deactivate()
   oTimer1:End()
   cLog += "after STOP : t1 active=" + cValToChar( oTimer1:lActive ) + ;
           " t2 active=" + cValToChar( oTimer2:lActive ) + CRLF

   oChild1:Timer( oTimer1:nId )
   oChild2:Timer( oTimer2:nId )
   cLog += "after STOP+events: fired1=" + cValToChar( nFired1 ) + ;
           " fired2=" + cValToChar( nFired2 ) + CRLF

   MemoWrit( "timertst2.log", cLog )

   oFrame:End()

return nil

//----------------------------------------------------------------------------//
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 7335
Joined: Thu Oct 18, 2012 07:17 PM

Re: MDI Child Timer conflict

Posted: Tue May 19, 2026 06:50 AM

Hi Antonio,

Thank you for the test! Your sample proves that mathematically and isolated, Windows correctly routes and kills independent timers across MDI child windows. However, this model misses the real-world GUI bottleneck occurring in my app.

In your test, the actions are just 'nFired1++' and 'nFired2++' (zero CPU/GUI load).

In my real application:

  1. Timer 1 (every 90ms) is pushing points to a heavy custom data-plotting window.
  2. Timer 2 (every 300ms) runs an ACTION that calls 'oBrw:SetArray( aData )' and 'oBrw:Refresh()' on an XBROWSE inside the second MDI Child.

The issue is NOT that TTimer:End() doesn't work in theory. The issue is a Windows Message Queue saturation/freeze caused by 'oBrw:Refresh()' ticking continuously at 300ms while the other window plots data.

When both windows are running, the continuous graphical invalidation (WM_PAINT paint-storms) causes the MDI frame or button bar to become completely unresponsive to physical user clicks. When I click the physical "STOP" button on the first window, the click event is either starved, dropped, or delayed by the event loop because the application is bottlenecked by the continuous XBROWSE redrawing.

If your test simulated a real loop updating an active XBROWSE with heavy painting on the other child, you would see that the physical mouse click on the STOP button fails to execute its ACTION block because the application is structurally choked.

What is the recommended approach in FiveWin to handle heavy asynchronous XBROWSE refreshing without starving mouse click events on native BUTTONBARs?

try this please

#include "FiveWin.ch"
#include "XBrowse.ch"

static oWndMain, oWndOsc, oWndDataInfo

//----------------------------------------------------------------//
function Main()
   local oFont

   DEFINE FONT oFont NAME "TAHOMA" SIZE 0,-12

   DEFINE WINDOW oWndMain TITLE "Timer Test" MDI MENU BuildMenu()
   oWndMain:SetFont( oFont )

   ACTIVATE WINDOW oWndMain MAXIMIZED

return nil

//----------------------------------------------------------------//
static function BuildMenu()
   local oMenu
   MENU oMenu
   MENUITEM "Tasks"
      MENU
         MENUITEM "New" ACTION New()
      ENDMENU
      oMenu:AddMDI()
   ENDMENU
return oMenu

//----------------------------------------------------------------//
static function New()
   local oBar, oMsgBar
   local oOsc          := "OGGETTO_FALSO_OSC" // Simula il tuo oggetto grafico
   local oTimer
   local nMode         := 1
   local nT            := 0.15

   if oWndOsc == nil

  DEFINE WINDOW oWndOsc MDICHILD OF oWndMain TITLE "New (Oscilloscope Window)"

  // Creiamo la ButtonBar esattamente come nel tuo codice
  oBar := NewBtnBar( @oOsc, oWndOsc, oTimer, nMode )

  // Pulsante per aprire la seconda finestra dei dati
  DEFINE BUTTON OF oBar NOBORDER ACTION Infodata( oOsc, nMode, nT )
  
  DEFINE BUTTON OF oBar PROMPT "Close" ACTION oWndOsc:End()

  DEFINE MSGBAR oMsgBar OF oWndOsc 2007

  ACTIVATE WINDOW oWndOsc MAXIMIZED ;
     VALID ( oWndOsc := nil, .T. )
   else
      oWndOsc:SetFocus()
   endif

return nil

//----------------------------------------------------------------//
static function NewBtnBar( oOsc, oWnd, oTimer, nMode )
   local oBar

   DEFINE BUTTONBAR oBar OF oWnd 2015 SIZE 80, 80 
   

   // Pulsante START
   DEFINE BUTTON OF oBar NOBORDER;
   PROMPT "START" ;
   ACTION testTime( oOsc, nMode, oWnd, @oTimer )

   // Pulsante STOP (Quello che smette di funzionare)
   DEFINE BUTTON OF oBar NOBORDER;
   PROMPT "STOP" ;
   ACTION If( oTimer != NIL, ( oTimer:Deactivate(), MsgInfo("Timer Fermato!") ), MsgInfo("Timer è NIL!") )

return oBar

//----------------------------------------------------------------//
FUNCTION testTime( oOsc, nMode, oWnd, oTimer )
   LOCAL nInterval := 90

   if oTimer != nil
      oTimer:End()
   endif

   DEFINE TIMER oTimer ;
   INTERVAL nInterval ;
   OF oWnd ;
   ACTION (  NIL ) // Simula il movimento dell'onda senza appesantire

   ACTIVATE TIMER oTimer

RETURN NIL

//----------------------------------------------------------------//
static function Infodata( oOsc, nChannel, nStepTime )
   local oBrw, oBar, oMsgBar
   local aData
   local oTimerInfo

   if oWndDataInfo == nil

  // Array di dati simulato
  aData := { {1, 0.15, 50.00}, {2, 0.30, 52.10}, {3, 0.45, 48.50} }

  DEFINE WINDOW oWndDataInfo MDICHILD OF oWndMain TITLE "Info data (XBROWSE Window)"

  @ 0, 0 XBROWSE oBrw OF oWndDataInfo LINES NOBORDER ;
     DATASOURCE aData ;
     COLUMNS 1, 2, 3 ;
     HEADERS "IDX", "TIME", "VALUE"

  oBrw:CreateFromCode()

  DEFINE BUTTONBAR oBar OF oWndDataInfo 2007 SIZE 70, 60
  DEFINE BUTTON OF oBar PROMPT "Close" ACTION oWndDataInfo:End()

  oWndDataInfo:oClient = oBrw

  DEFINE MSGBAR oMsgBar OF oWndDataInfo 2007

  // Timer indipendente a 300ms che aggiorna l'array
  DEFINE TIMER oTimerInfo ;
         INTERVAL 300 ;
         OF oWndDataInfo ;
         ACTION UpdateSamples( @oBrw, oOsc, nChannel, nStepTime )
  
  ACTIVATE TIMER oTimerInfo

  ACTIVATE WINDOW oWndDataInfo MAXIMIZED ;
      VALID ( oWndDataInfo := nil, .T. )
   else
      oWndDataInfo:SetFocus()
   endif

return nil

//----------------------------------------------------------------//
FUNCTION UpdateSamples( oBrw, oOsc, nMode, nT )
   local aNewData := { {1, hb_Random(1,10), hb_Random(10,100)},;
                       {2, hb_Random(1,10), hb_Random(10,100)},;
                       {3, hb_Random(1,10), hb_Random(10,100)} }

   // Forza il rinfresco continuo di XBROWSE
   oBrw:SetArray( aNewData )
   oBrw:Refresh()

RETURN NI

If you return on first child then the timer is nil and you cannot stop it

Since from 1991/1992 ( fw for clipper Rel. 14.4 - Momos)

I use : FiveWin for Harbour March-April 2024 - Harbour 3.2.0dev (harbour_bcc770_32_20240309) - Bcc7.70 - xMate ver. 1.15.3 - PellesC - mail: silvio[dot]falconi[at]gmail[dot]com
Posts: 7335
Joined: Thu Oct 18, 2012 07:17 PM

Re: MDI Child Timer conflict

Posted: Tue May 19, 2026 07:10 AM

Perhaps I found the solution

#include "FiveWin.ch"
#include "XBrowse.ch"

static oWndMain, oWndOsc, oWndDataInfo

//----------------------------------------------------------------//
function Main()
   local oFont

   DEFINE FONT oFont NAME "TAHOMA" SIZE 0,-12

   DEFINE WINDOW oWndMain TITLE "Fivewin MDI Test - Double XBrowse Timer" MDI MENU BuildMenu()
   oWndMain:SetFont( oFont )

   ACTIVATE WINDOW oWndMain MAXIMIZED

return nil

//----------------------------------------------------------------//
static function BuildMenu()
   local oMenu
   MENU oMenu
   MENUITEM "Tasks"
      MENU
         MENUITEM "New" ACTION New()
      ENDMENU
      oMenu:AddMDI()
   ENDMENU
return oMenu

//----------------------------------------------------------------//
static function New()
   local oBar, oMsgBar, oBrwOsc
   local oOsc          := "OGGETTO_FALSO_OSC"
   local aTimer        := { NIL } // Array locale che blinda il timer del primo Child
   local nMode         := 1
   local nT            := 0.15
   local aGraphData    := {}      // Array che conterrà i punti dell'onda dell'oscilloscopio
   local aContatore    := { 0.0 } // Array per l'avanzamento del tempo nel timer

   // Popoliamo l'array iniziale dell'oscilloscopio con 12 righe
   local i
   for i := 1 to 12
      AAdd( aGraphData, { i, "------------------------" } )
   next

   if oWndOsc == nil

  DEFINE WINDOW oWndOsc MDICHILD OF oWndMain TITLE "New (Oscilloscope Simulation)"

  // L'XBrowse nel primo Child che simulerà graficamente l'onda che cammina
  @ 0, 0 XBROWSE oBrwOsc OF oWndOsc LINES NOBORDER ;
     DATASOURCE aGraphData ;
     COLUMNS 1, 2 ;
     HEADERS "CH", "OSCILLOSCOPE SIGNAL DISPLAY (90ms)" ;
     COLSIZES 50, 500
  
  oBrwOsc:CreateFromCode()
  oWndOsc:oClient := oBrwOsc

  // Passiamo gli elementi alla barra dei pulsanti
  oBar := NewBtnBar( @oOsc, oWndOsc, aTimer, nMode, oBrwOsc, aGraphData, aContatore )

  DEFINE BUTTON OF oBar NOBORDER PROMPT "INFO DATA" ACTION Infodata( oOsc, nMode, nT )
  DEFINE BUTTON OF oBar PROMPT "Close" ACTION oWndOsc:End()

  DEFINE MSGBAR oMsgBar OF oWndOsc 2007

  ACTIVATE WINDOW oWndOsc MAXIMIZED ;
     VALID ( If( aTimer[1] != NIL, ( aTimer[1]:End(), aTimer[1] := NIL ), NIL ), oWndOsc := nil, .T. )
   else
      oWndOsc:SetFocus()
   endif

return nil

//----------------------------------------------------------------//
static function NewBtnBar( oOsc, oWnd, aTimer, nMode, oBrwOsc, aGraphData, aContatore )
   local oBar

   DEFINE BUTTONBAR oBar OF oWnd 2015 SIZE 80, 80 

   // START - Fa camminare l'onda nell'XBrowse dell'oscilloscopio
   DEFINE BUTTON OF oBar NOBORDER;
   PROMPT "START" ;
   ACTION testTime( oOsc, nMode, oWnd, aTimer, oBrwOsc, aGraphData, aContatore )

   // STOP - Lo ferma istantaneamente senza risentire del focus di Infodata
   DEFINE BUTTON OF oBar NOBORDER;
   PROMPT "STOP" ;
   ACTION ( ;
      If( aTimer[1] != NIL, ;
          ( aTimer[1]:Deactivate(), aTimer[1]:End(), aTimer[1] := NIL, MsgInfo("Timer Oscilloscopio FERMATO!") ), ;
          MsgInfo( "Il timer era già NIL o spento!" ) ;
      ) ;
   )

return oBar

//----------------------------------------------------------------//
FUNCTION testTime( oOsc, nMode, oWnd, aTimer, oBrwOsc, aGraphData, aContatore )
   LOCAL nInterval := 90
   LOCAL oTmpTimer

   if aTimer[1] != nil
      aTimer[1]:End()
      aTimer[1] := nil
   endif

   // Questo timer fa camminare l'oscilloscopio aggiornando l'XBrowse ogni 90ms
   DEFINE TIMER oTmpTimer ;
   INTERVAL nInterval ;
   OF oWnd ;
   ACTION ( ;
      aContatore[1] += 0.25, ;
      AggiornaGraficoSimulato( oBrwOsc, aGraphData, aContatore[1] ) ;
   )

   ACTIVATE TIMER oTmpTimer

   aTimer[1] := oTmpTimer

RETURN NIL

//----------------------------------------------------------------//
// Muove l'onda sinusoidale e forza il riallineamento dell'XBrowse
static function AggiornaGraficoSimulato( oBrwOsc, aGraphData, nVal )
   local i, nPos
   local cBase := "------------------------"

   for i := 1 to Len( aGraphData )
      // Sfasamento matematico per generare un'onda sinusoide fluida che si muove nel tempo
      nPos := Int( ( Sin( nVal + (i * 0.5) ) + 1 ) * 11 ) + 1
      aGraphData[i][2] := SubStr( cBase, 1, nPos - 1 ) + "X" + SubStr( cBase, nPos + 1 )
   next

   // ADESSO SI MUOVE: Diciamo all'XBrowse che l'array è cambiato internamente
   oBrwOsc:SetArray( aGraphData )
   oBrwOsc:Refresh()
return nil

//----------------------------------------------------------------//
static function Infodata( oOsc, nChannel, nStepTime )
   local oBrw, oBar, oMsgBar
   local aData
   local oTimerInfo

   if oWndDataInfo == nil
      

  aData := { {1, 0.15, 50.00}, {2, 0.30, 52.10}, {3, 0.45, 48.50} }

  DEFINE WINDOW oWndDataInfo MDICHILD OF oWndMain TITLE "Info data (XBROWSE Window)"

  @ 0, 0 XBROWSE oBrw OF oWndDataInfo LINES NOBORDER ;
     DATASOURCE aData ;
     COLUMNS 1, 2, 3 ;
     HEADERS "IDX", "TIME", "VALUE"
  
  oBrw:CreateFromCode()

  DEFINE BUTTONBAR oBar OF oWndDataInfo 2007 SIZE 70, 60
  DEFINE BUTTON OF oBar PROMPT "Close" ACTION oWndDataInfo:End()

  oWndDataInfo:oClient = oBrw

  DEFINE MSGBAR oMsgBar OF oWndDataInfo 2007

  // Secondo Timer a 300ms che aggiorna i dati numerici nell'altro Child
  DEFINE TIMER oTimerInfo ;
         INTERVAL 300 ;
         OF oWndDataInfo ;
         ACTION UpdateSamples( oBrw, oOsc, nChannel, nStepTime )
  
  ACTIVATE TIMER oTimerInfo

  ACTIVATE WINDOW oWndDataInfo MAXIMIZED ;
      VALID ( oTimerInfo:End(), oWndDataInfo := nil, .T. )
   else
      oWndDataInfo:SetFocus()
   endif

return nil

//----------------------------------------------------------------//
FUNCTION UpdateSamples( oBrw, oOsc, nMode, nT )
   local aNewData := { {1, hb_Random(1,10), hb_Random(10,100)},;
                       {2, hb_Random(1,10), hb_Random(10,100)},;
                       {3, hb_Random(1,10), hb_Random(10,100)} }

   oBrw:SetArray( aNewData )
   oBrw:Refresh()

RETURN NIL
Since from 1991/1992 ( fw for clipper Rel. 14.4 - Momos)

I use : FiveWin for Harbour March-April 2024 - Harbour 3.2.0dev (harbour_bcc770_32_20240309) - Bcc7.70 - xMate ver. 1.15.3 - PellesC - mail: silvio[dot]falconi[at]gmail[dot]com

Continue the discussion