FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin for Harbour/xHarbour xBrowse GoTo
Posts: 218
Joined: Mon Feb 07, 2022 09:54 PM
xBrowse GoTo
Posted: Tue Dec 09, 2025 09:26 PM

Hi all,
with great help from this forum I created an xBrowse with a tree to visualize a one to many relationship.
Everything is working fine. I can add tasks and detail records and the tree is building up correctly.

My only problem is to position the selected line of the browse.
I made a screenhot for clarification:

The actual line of oBrw is record # 3 of details.dbf
When pressing the button | + Detail | a new record is appended and BuildTree() is called.
To show the new line I have to call oBrw:GoBottom() and oBrw:Refresh().
But than the hilighted row is of course at the bottom.
And the selected row of oBrw line # 6.

I tried several ways to keep the selected line 3 in view. but I couldn't succeed.
I read about xBrowse bookmarks. Perhaps this could help.nBut I didn't understand how to handle with boomarks.

Could anyone give me a clue about this?
Thanks, Detlef

Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: xBrowse GoTo
Posted: Wed Dec 10, 2025 04:58 AM

Dear Detlef,

A tree has opened branches and not opened ones. So we need to save those states before we rebuild it
so later we can restore the same states.

In order to save them, we need to steep through all the tree branches:

FUNCTION SaveTreeState( oTree )  

   LOCAL aState := {}  

   LOCAL oItem  
     

   oItem := oTree:oFirst  

   WHILE oItem != NIL  

      AADD( aState, { oItem:lOpened, oItem:Cargo } )
      oItem := oItem:Skip( 1 )  

   END  
     

RETURN aState
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: xBrowse GoTo
Posted: Wed Dec 10, 2025 05:06 AM

Now, in order to restore such state we need to use this function:

FUNCTION RestoreTreeState( oTree, aState )
   LOCAL oItem := oTree:oFirst
   LOCAL nAt

   WHILE oItem != NIL
      // We check if this item Cargo exists in the save states array
      // We compare type and ID (elements 1 and 2 from the Cargo array)
      nAt := AScan( aState, { | a | ValType( a[ 2 ] ) == "A" .and. ;
                                    ValType( oItem:Cargo ) == "A" .and. ;
                                    a[ 2 ][ 1 ] == oItem:Cargo[ 1 ] .and. ;
                                    a[ 2 ][ 2 ] == oItem:Cargo[ 2 ] } )

  // If found and opened (.T.), we open it
  IF nAt > 0 .and. aState[ nAt ][ 1 ] 
     oItem:Open()
  ENDIF

  // We go to the next. As we opened above, Skip(1) will step through its childs
  oItem := oItem:Skip( 1 )
   END
RETURN NIL
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: xBrowse GoTo
Posted: Wed Dec 10, 2025 05:14 AM

In order to get a cleaner code we reorganize the code like this:

function BuildBar( oDlg, oBrw )

   local oBar

   DEFINE BUTTONBAR oBar OF oDlg SIZE 50, 50 2007

   DEFINE BUTTON OF oBar PROMPT "Task" RESOURCE "add" ;
      ACTION AddTask( oBrw )

   DEFINE BUTTON OF oBar PROMPT "Detail" RESOURCE "add" ;
      ACTION AddDetail( oBrw )

   DEFINE BUTTON OF oBar PROMPT "Edit" RESOURCE "edit" ACTION oBrw:Edit()

   DEFINE BUTTON OF oBar PROMPT "Del" RESOURCE "del"
   

   oDlg:Resize()
 
return .T.
// Function to save the current status (Open/Closed) of the tree items
FUNCTION SaveTreeState( oTree )
   LOCAL aState := {}
   LOCAL oItem
     

   oItem := oTree:oFirst
   WHILE oItem != NIL
      // Save { IsOpen, CargoData }
      AADD( aState, { oItem:lOpened, oItem:Cargo } )
      oItem := oItem:Skip( 1 )
   END
RETURN aState

// Function to restore the status (Open/Closed) to a new tree
FUNCTION RestoreTreeState( oTree, aState )
   LOCAL oItem := oTree:oFirst
   LOCAL nAt

   WHILE oItem != NIL
      // Search if this item's 'Cargo' exists in our saved state array
      // Note: We compare Type and ID (elements 1 and 2 of Cargo array)
      nAt := AScan( aState, { | a | ValType( a[ 2 ] ) == "A" .and. ;
                                    ValType( oItem:Cargo ) == "A" .and. ;
                                    a[ 2 ][ 1 ] == oItem:Cargo[ 1 ] .and. ;
                                    a[ 2 ][ 2 ] == oItem:Cargo[ 2 ] } )

  // If found and it was marked as opened (.T.), we open it in the new tree
  IF nAt > 0 .and. aState[ nAt ][ 1 ] 
     oItem:Open()
  ENDIF

  // Move to next. Calling Open() above makes Skip(1) enter the children automatically
  oItem := oItem:Skip( 1 )
   END
RETURN NIL

// Helper to update tree without collapsing everything
FUNCTION UpdateTree( oBrw, nBrwPos )
   LOCAL aState

   // 1. Save which branches are open before destroying the tree
   aState := SaveTreeState( oBrw:oTree )

   // 2. Rebuild the tree (this creates a new, fully collapsed tree)
   oBrw:oTree := BuildTree()

   // 3. Restore the previously open branches
   RestoreTreeState( oBrw:oTree, aState )

   // 4. Restore cursor position and refresh
   oBrw:nRowSel := nBrwPos
   oBrw:Refresh()
   

RETURN NIL
// Action to add a new Task
STATIC FUNCTION AddTask( oBrw )
   LOCAL aState

   // 1. Save current state
   aState := SaveTreeState( oBrw:oTree )

   // 2. Database logic
   Tasks->( DbAppend() )
   Details->( DbAppend() )
   Details->Task_id := Tasks->Id
   Details->Date    := Date()
   Details->From    := "09:00"
   Details->To      := "11:00"

   // 3. Rebuild the tree
   oBrw:oTree := BuildTree()

   // 4. Restore what was previously open
   RestoreTreeState( oBrw:oTree, aState )
   

   oBrw:Refresh()
RETURN NIL

// Action to add a new Detail
STATIC FUNCTION AddDetail( oBrw )
   LOCAL aState
   LOCAL nTaskRec

   // Check if there are items before trying to read Cargo
   IF oBrw:oTreeItem == NIL; RETURN NIL; ENDIF

   // Get Parent Task RecNo (regardless of current position - whether on task or detail)
   nTaskRec := If( Len( oBrw:oTreeItem:Cargo ) == 2,;
                   oBrw:oTreeItem:Cargo[ 2 ],;
                   oBrw:oTreeItem:Cargo[ 3 ] )

   // 1. Save current state
   aState := SaveTreeState( oBrw:oTree )

   // --- TRICK ---
   // If we add a detail, we WANT the parent task branch to be visible/open.
   // Modify 'aState' to force this task as 'Open' (.T.) before restoring.
   ForceParentOpen( aState, nTaskRec )
   // -------------

   // 2. Database logic
   Tasks->( DbGoTo( nTaskRec ) )
   Details->( DbAppend() )
   Details->task_id := Tasks->id
   Details->Date    := Date()
   Details->From    := "09:00"
   Details->To      := "11:00"

   // 3. Rebuild tree
   oBrw:oTree := BuildTree()

   // 4. Restore (now includes our forced open task)
   RestoreTreeState( oBrw:oTree, aState )

   oBrw:Refresh()
RETURN NIL

// Helper to ensure the task where we are adding a detail is open
STATIC FUNCTION ForceParentOpen( aState, nTaskRec )
   LOCAL nAt

   // Search in state array if that specific Task exists
   nAt := AScan( aState, { | a | a[ 2 ][ 1 ] == "task" .and. a[ 2 ][ 2 ] == nTaskRec } )

   IF nAt > 0
      // If exists, force it to be Open (.T.)
      aState[ nAt ][ 1 ] := .T.
   ELSE
      // If for some reason it wasn't there (rare), add it forced as open
      AAdd( aState, { .T., { "task", nTaskRec } } )
   ENDIF
RETURN NIL
@ 10, 14 BUTTON "OK" OF oDlg SIZE 80, 25 ;
      ACTION ( Details->( DbCommit() ), oDlg:End(),;
               UpdateTree( oBrw, nBrwPos ) ) // <--- Using the helper
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: xBrowse GoTo
Posted: Wed Dec 10, 2025 05:16 AM

Full source code: (waiting for your feedback)

detlef.prg

#include "FiveWin.ch"

REQUEST DbfCdx

FUNCTION Main()

   LOCAL oDlg, oBrw

   // 1. Database Creation Checks
   IF !File( "tasks.dbf" )
      DbCreate( "tasks.dbf", { ;
         { "ID",   "+",  10, 0 }, ;
         { "TASK", "C",  50, 0 } } )
   ENDIF   

   IF !File( "details.dbf" )
      DbCreate( "details.dbf", { ;
         { "ID",      "N", 10, 0 }, ;
         { "TASK_ID", "N", 10, 0 },;
         { "DATE",    "D",  8, 0 },;
         { "FROM",    "C", 50, 0 },;
         { "TO",      "C", 50, 0 } } )
   ENDIF   

   // 2. Open Databases and Indexes
   USE tasks VIA "DBFCDX"

   IF Tasks->( RecCount() ) == 0
      Tasks->( DbAppend() )
      Tasks->Task := "Project Alpha"
   ENDIF   

   INDEX ON field->ID TO "ID"
   SET ORDER TO TAG ID

   USE details VIA "DBFCDX" NEW

   IF Details->( RecCount() ) == 0
      Details->( DbAppend() )
      Details->Task_id   := Tasks->Id
      Details->Date      := Date()
      Details->From      := "09:00"
      Details->To        := "11:00"
   ENDIF   

   INDEX ON field->Task_id TO "TASK_ID"
   SET ORDER TO TAG TASK_ID

   SELECT "tasks"
   SET RELATION TO details->Task_id INTO details

   // 3. UI Setup
   DEFINE DIALOG oDlg SIZE 1200, 400 PIXEL TRUEPIXEL

   @ 2.5, 2 XBROWSE oBrw SIZE 1170, 350 OF oDlg ;
      DATASOURCE "Tasks" ;
      CELL LINES NOBORDER
   

   oBrw:CreateFromCode()
   oBrw:SetTree( BuildTree() )
   oBrw:aCols[ 1 ]:nWidth := 500
   

   // Edit action
   oBrw:bEdit = { | o | EditItem( o:oBrw:oTreeItem, o:oBrw ) }
   

   // Navigation action (Sync DB cursors)
   oBrw:bChange = { | o | If( Len( o:oTreeItem:Cargo ) == 2,;
                          Tasks->( DbGoTo( o:oTreeItem:Cargo[ 2 ] ) ),;
                          Details->( DbGoTo( o:oTreeItem:Cargo[ 2 ] ) ) ) }
   

   oDlg:oClient = oBrw

   ACTIVATE DIALOG oDlg CENTERED ;
      ON INIT BuildBar( oDlg, oBrw ) 

RETURN NIL

//----------------------------------------------------------------------------//

FUNCTION BuildBar( oDlg, oBrw )

   LOCAL oBar

   DEFINE BUTTONBAR oBar OF oDlg SIZE 50, 50 2007

   // Modified: Call static functions instead of inline code blocks
   DEFINE BUTTON OF oBar PROMPT "Task" RESOURCE "add" ;
      ACTION AddTask( oBrw )

   DEFINE BUTTON OF oBar PROMPT "Detail" RESOURCE "add" ;
      ACTION AddDetail( oBrw ) 

   DEFINE BUTTON OF oBar PROMPT "Edit" RESOURCE "edit" ACTION oBrw:Edit()

   DEFINE BUTTON OF oBar PROMPT "Del" RESOURCE "del"
   

   oDlg:Resize()
 
RETURN .T.

//----------------------------------------------------------------------------//

FUNCTION BuildTree()

   LOCAL oTree, nRecNo := Tasks->( RecNo() )

   IF Tasks->( RecCount() ) > 0
      TREE oTree  

      Details->( DbGoTop() )
      Tasks->( DbGoTop() )
      

  WHILE !Tasks->( Eof() )  
     // Cargo format: { "task", TaskRecNo }
     TREEITEM tasks->Task CARGO { "task", Tasks->( RecNo() ) }
     
     IF Details->( RecCount() ) > 0
        TREE  
           WHILE Details->Task_id == tasks->id .and. !Details->( Eof() )  
              // Cargo format: { "detail", DetailRecNo, TaskRecNo }
              TREEITEM "Date: " + DToC( Details->Date ) + ;
                       " - From: " + RTRim( Details->From ) + ;
                       " - To: " + RTrim( Details->To ) ;
                       CARGO { "detail", Details->( RecNo() ), Tasks->( RecNo() ) }
              
              Details->( DbSkip() )  
           END  
        ENDTREE  
     ENDIF    
     
     Tasks->( DbSkip() )  
  END  
  ENDTREE  
  
  Tasks->( DbGoTo( nRecNo ) )
  
   ELSE 
      TREE oTree
      ENDTREE
   ENDIF 

RETURN oTree

//----------------------------------------------------------------------------//

FUNCTION EditItem( oTreeItem, oBrw )

   LOCAL nRec := oTreeItem:Cargo[ 2 ], oDlg
   LOCAL nBrwPos := oBrw:nRowSel

   IF oTreeItem:Cargo[ 1 ] == "detail"
      DEFINE DIALOG oDlg SIZE 400, 220 PIXEL TRUEPIXEL TITLE "Edit detail"

  Details->( DbGoTo( nRec ) )

  @ 1, 2 SAY "Date:" OF oDlg 
  @ 1, 6 GET Details->Date PICTURE "9999-99-99" OF oDlg

  @ 3.2, 2 SAY "From:" OF oDlg 
  @ 3.5, 6 GET Details->From PICTURE "99:99" OF oDlg

  @ 5.3, 2 SAY "To:" OF oDlg
  @ 5.9, 6 GET Details->To PICTURE "99:99" OF oDlg

  @ 10, 14 BUTTON "OK" OF oDlg SIZE 80, 25 ;
     ACTION ( Details->( DbCommit() ), oDlg:End(),;
              UpdateTree( oBrw, nBrwPos ) ) // Modified: Use UpdateTree helper

  @ 10, 34 BUTTON "Cancel" OF oDlg SIZE 80, 25 ACTION oDlg:End()

  ACTIVATE DIALOG oDlg CENTERED
   ELSE   

      DEFINE DIALOG oDlg SIZE 400, 220 PIXEL TRUEPIXEL TITLE "Edit task"

  Tasks->( DbGoTo( nRec ) )

  @ 1, 2 SAY "Task:" OF oDlg 
  @ 1, 6 GET Tasks->Task SIZE 340, 20 OF oDlg

  @ 10, 14 BUTTON "OK" OF oDlg SIZE 80, 25 ;
     ACTION ( Tasks->( DbCommit() ), oDlg:End(),;
              UpdateTree( oBrw, nBrwPos ) ) // Modified: Use UpdateTree helper

  @ 10, 34 BUTTON "Cancel" OF oDlg SIZE 80, 25 ACTION oDlg:End()

  ACTIVATE DIALOG oDlg CENTERED
   ENDIF

RETURN NIL

//----------------------------------------------------------------------------//
//  NEW HELPER FUNCTIONS FOR TREE STATE MANAGEMENT
//----------------------------------------------------------------------------//

// Action to add a new Task
STATIC FUNCTION AddTask( oBrw )
   LOCAL aState

   // 1. Save current state
   aState := SaveTreeState( oBrw:oTree )

   // 2. Database logic
   Tasks->( DbAppend() )
   Details->( DbAppend() )
   Details->Task_id := Tasks->Id
   Details->Date    := Date()
   Details->From    := "09:00"
   Details->To      := "11:00"

   // 3. Rebuild the tree
   oBrw:oTree := BuildTree()

   // 4. Restore what was previously open
   RestoreTreeState( oBrw:oTree, aState )
   

   oBrw:Refresh()
RETURN NIL

// Action to add a new Detail
STATIC FUNCTION AddDetail( oBrw )
   LOCAL aState
   LOCAL nTaskRec

   // Check if there are items before trying to read Cargo
   IF oBrw:oTreeItem == NIL; RETURN NIL; ENDIF

   // Get Parent Task RecNo (regardless of current position)
   nTaskRec := If( Len( oBrw:oTreeItem:Cargo ) == 2,;
                   oBrw:oTreeItem:Cargo[ 2 ],;
                   oBrw:oTreeItem:Cargo[ 3 ] )

   // 1. Save current state
   aState := SaveTreeState( oBrw:oTree )

   // --- TRICK ---
   // Force the parent task to be Open (.T.) in the state array
   ForceParentOpen( aState, nTaskRec )
   // -------------

   // 2. Database logic
   Tasks->( DbGoTo( nTaskRec ) )
   Details->( DbAppend() )
   Details->task_id := Tasks->id
   Details->Date    := Date()
   Details->From    := "09:00"
   Details->To      := "11:00"

   // 3. Rebuild tree
   oBrw:oTree := BuildTree()

   // 4. Restore (now includes our forced open task)
   RestoreTreeState( oBrw:oTree, aState )

   oBrw:Refresh()
RETURN NIL

// Helper to update tree without collapsing everything (Used in EditItem)
FUNCTION UpdateTree( oBrw, nBrwPos )
   LOCAL aState

   // 1. Save state
   aState := SaveTreeState( oBrw:oTree )

   // 2. Rebuild
   oBrw:oTree := BuildTree()

   // 3. Restore
   RestoreTreeState( oBrw:oTree, aState )

   // 4. Restore cursor position and refresh
   oBrw:nRowSel := nBrwPos
   oBrw:Refresh()
   

RETURN NIL

// Helper to ensure the task where we are adding a detail is open
STATIC FUNCTION ForceParentOpen( aState, nTaskRec )
   LOCAL nAt

   // Search in state array if that specific Task exists
   nAt := AScan( aState, { | a | a[ 2 ][ 1 ] == "task" .and. a[ 2 ][ 2 ] == nTaskRec } )

   IF nAt > 0
      // If exists, force it to be Open (.T.)
      aState[ nAt ][ 1 ] := .T.
   ELSE
      // If not found, add it forced as open
      AAdd( aState, { .T., { "task", nTaskRec } } )
   ENDIF
RETURN NIL

// Function to save the current status (Open/Closed) of the tree items
FUNCTION SaveTreeState( oTree )
   LOCAL aState := {}
   LOCAL oItem
     

   oItem := oTree:oFirst
   WHILE oItem != NIL
      // Save { IsOpen, CargoData }
      AADD( aState, { oItem:lOpened, oItem:Cargo } )
      oItem := oItem:Skip( 1 )
   END
RETURN aState

// Function to restore the status (Open/Closed) to a new tree
FUNCTION RestoreTreeState( oTree, aState )
   LOCAL oItem := oTree:oFirst
   LOCAL nAt

   WHILE oItem != NIL
      // Search if this item's 'Cargo' exists in our saved state array
      // Cargo structure: [1] = Type ("task"/"detail"), [2] = ID
      nAt := AScan( aState, { | a | ValType( a[ 2 ] ) == "A" .and. ;
                                    ValType( oItem:Cargo ) == "A" .and. ;
                                    a[ 2 ][ 1 ] == oItem:Cargo[ 1 ] .and. ;
                                    a[ 2 ][ 2 ] == oItem:Cargo[ 2 ] } )

  // If found and it was marked as opened (.T.), we open it in the new tree
  IF nAt > 0 .and. aState[ nAt ][ 1 ] 
     oItem:Open()
  ENDIF

  // Move to next. Calling Open() above makes Skip(1) enter the children automatically
  oItem := oItem:Skip( 1 )
   END
RETURN NIL
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 218
Joined: Mon Feb 07, 2022 09:54 PM
Re: xBrowse GoTo
Posted: Wed Dec 10, 2025 11:30 AM

Many thanks for your help, Antonio.

I appreciate your explanations how to manage trees because until now I had poor knowledge about this. In my case I need a tree whose branches are always open. No need to close them.

I tried your source code. Unfortunately I get an error 9009, hbx_realloc can't reallocate memory.

Regards, Detlef

Continue the discussion