updated: Simple and useful form layout designer for DBase / SQLite and others

Share your advanced PureBasic knowledge/code with the community.
moricode
Enthusiast
Enthusiast
Posts: 167
Joined: Thu May 25, 2023 3:55 am

updated: Simple and useful form layout designer for DBase / SQLite and others

Post by moricode »

Now Upgrade to the new advance version 1.10 :

I use this tools to build DBase IV / SQLite Record View/Edit forms , save the design layout, load the layout position in your database program and read the SQLite table and put in the layout field, it is very convenience . before this , i was hardcode the record fields position by hand .


Feature added :
1. Added selection box for easy multiple object selection
2. Added [Select ALL],[Select Label], [Select Edit] Button for fast selection
3. use proper mouse icon when mouse hover over Selected Objects/ Object Resize Corner
4. Fixed new object placement position , now it will not overlaps or cover other objects when add new
5. smart object placement start from vertical direction first then to next horizontal column if reach canvas bottom
6. Fixed to smooth object movement , no more jerky step

scroll down this page , or here
Updated Version 1.10 :
viewtopic.php?t=88159#p649901

The OLD V1.0 code is still remain here for new people to compare the code and test it , may be you find some use too. :)

Self explain code , simple modification can do flow chart designer or event other use in your own application , eg. powerpoint is not a dream ...
Example code for learning purpose
just do what you want , don't mention ...

best regards

Code: Select all

; Visual Form Designer in PureBasic 5.7x , 6.1x , 6.2x
; Features: BTN, LABEL, EDIT, PHOTO objects with 5px grid snapping

#Version$ = "1.0 pro"
Structure FormObject
  Type.s
  X.l
  Y.l
  Width.l
  Height.l
  Text.s
  ImagePath.s
  isSelected.l
  SelectionOrder.l
EndStructure

#GRID_SIZE = 5
#MAX_OBJECTS = 100

Global NewList Objects.FormObject()
Global DragMode.i = #False
Global ResizeMode.i = #False
Global ResizeEdge.s = ""
Global DragStartX.l, DragStartY.l
Global DragObjX.l, DragObjY.l
Global DragObjW.l, DragObjH.l
Global OffscreenImage.i
Global SelectionCounter.l = 0
Global PendingClickObj.i = -1
Global PendingCtrlPressed.i = #False
Global HasMoved.i = #False
Global ButtonCounter.l = 0
Global LabelCounter.l = 0
Global EditCounter.l = 0
Global PhotoCounter.l = 0

#RESIZE_HANDLE = 8

Enumeration
  #Win_Main
  #Canvas_Designer  
  #Text_Info
  #Btn_AddButton
  #Btn_AddLabel
  #Btn_AddEdit
  #Btn_AddPhoto
  #Btn_Delete
  #Btn_Save
  #Btn_SaveClean
  #Btn_Load
  #Btn_New
  #Btn_AlignLeft
  #Btn_AlignRight
  #Btn_AlignTop
  #Btn_AlignBottom
  #Btn_DistributeV
  #Btn_MatchSize
  #Btn_Renumber
  #Text_Spacing
  #String_Spacing
  #LABEL_OBJECT
  #LABEL_ALIGNMENT
  #LABEL_FILE  
EndEnumeration

Procedure SnapToGrid(Value.l)
  ProcedureReturn Round(Value / #GRID_SIZE, #PB_Round_Nearest) * #GRID_SIZE
EndProcedure

Procedure IsSelected(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
    IF Objects()\isSelected = 1
      ProcedureReturn #True
    ENDIF
  ProcedureReturn #False
EndProcedure

Procedure CheckTotalSelected()
   PROTECTED Total.l = 0
   ForEach Objects()
      IF Objects()\isSelected
         Total = Total + 1
      ENDIF
   Next
   ProcedureReturn Total
EndProcedure

Procedure CheckSelected()
   ForEach Objects()
      IF Objects()\isSelected
         ProcedureReturn ListIndex(Objects())
      ENDIF                      
   Next
   ProcedureReturn -1
EndProcedure

Procedure GetFirstSelectedIndex()
   PROTECTED MinOrder.l = 999999
   PROTECTED FirstIdx.l = -1
   PROTECTED idx.l = 0
   
   ForEach Objects()
      IF Objects()\isSelected
         IF Objects()\SelectionOrder < MinOrder
            MinOrder = Objects()\SelectionOrder
            FirstIdx = idx
         ENDIF
      ENDIF
      idx + 1
   Next
   ProcedureReturn FirstIdx
EndProcedure

Procedure AddSelection(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   Objects()\isSelected = 1
   SelectionCounter + 1
   Objects()\SelectionOrder = SelectionCounter
EndProcedure

Procedure RemoveSelection(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   Objects()\isSelected = 0
   Objects()\SelectionOrder = 0
EndProcedure

Procedure ClearSelection()
   ForEach Objects()
      Objects()\isSelected = 0
      Objects()\SelectionOrder = 0
   Next
   SelectionCounter = 0
EndProcedure

Procedure DrawGrid()
  IF StartDrawing(ImageOutput(OffscreenImage))
    DrawingMode(#PB_2DDrawing_Default)
    Box(0, 0, OutputWidth(), OutputHeight(), RGB(255, 255, 255))
    
    For x.l = 0 To OutputWidth() Step #GRID_SIZE
      For y.l = 0 To OutputHeight() Step #GRID_SIZE
        LineXY(x, y, x, y, RGB(220, 220, 220))
      Next
    Next
    StopDrawing()
  ENDIF
EndProcedure

Procedure DrawObjects()
   IF StartDrawing(ImageOutput(OffscreenImage))
      ForEach Objects()
         SELECT Objects()\Type
            CASE "BTN"
               DrawingMode(#PB_2DDrawing_Default)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(240, 240, 240))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(0, 0, 0))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X + 5, Objects()\Y + 5, Objects()\Text, RGB(0, 0, 0))
            CASE "LABEL"
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X, Objects()\Y, Objects()\Text, RGB(0, 0, 0))               
            CASE "EDIT"
               DrawingMode(#PB_2DDrawing_Default)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(255, 255, 255))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(128, 128, 128))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X + 3, Objects()\Y + 3, Objects()\Text, RGB(128, 128, 128))               
            CASE "PHOTO"
               DrawingMode(#PB_2DDrawing_Default)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(200, 200, 200))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(0, 0, 0))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X + Objects()\Width/2 - 25, Objects()\Y + Objects()\Height/2 - 5, Objects()\Text, RGB(100, 100, 100))               
         ENDSELECT
         IF Objects()\isSelected  
            DrawingMode(#PB_2DDrawing_Outlined)
            Box(Objects()\X - 2, Objects()\Y - 2, Objects()\Width + 4, Objects()\Height + 4, RGB(0, 120, 215))
            DrawingMode(#PB_2DDrawing_Default)
            Box(Objects()\X + Objects()\Width - #RESIZE_HANDLE, Objects()\Y + Objects()\Height - #RESIZE_HANDLE, #RESIZE_HANDLE, #RESIZE_HANDLE, RGB(0, 120, 215))
            Box(Objects()\X + Objects()\Width - #RESIZE_HANDLE + 1, Objects()\Y + Objects()\Height - #RESIZE_HANDLE + 1, #RESIZE_HANDLE - 2, #RESIZE_HANDLE - 2, RGB(255, 255, 255))   
         ENDIF    
      Next      
      StopDrawing()
   ENDIF
EndProcedure

Procedure AddObject(Type.s)
  AddElement(Objects())
  Objects()\Type = Type
  Objects()\X = 20
  Objects()\Y = 20
  
  SELECT Type
    CASE "BTN"
      Objects()\Width = 100
      Objects()\Height = 22
      ButtonCounter + 1
      Objects()\Text = "Button " + RSet(Str(ButtonCounter), 2, "0")
    CASE "LABEL"
      Objects()\Width = 60
      Objects()\Height = 20
      LabelCounter + 1
      Objects()\Text = "Label " + RSet(Str(LabelCounter), 2, "0")
    CASE "EDIT"
      Objects()\Width = 150
      Objects()\Height = 20
      EditCounter + 1
      Objects()\Text = "Edit " + RSet(Str(EditCounter), 2, "0")
    CASE "PHOTO"
      Objects()\Width = 100
      Objects()\Height = 100
      PhotoCounter + 1
      Objects()\Text = "Photo " + RSet(Str(PhotoCounter), 2, "0")
  ENDSELECT
  
  ClearSelection()
  AddSelection(ListSize(Objects()) - 1)
EndProcedure

Procedure FindObjectAt(X.l, Y.l)
  idx = ListSize(Objects()) - 1
  LastElement(Objects())
  
  Repeat
    IF X >= Objects()\X And X <= Objects()\X + Objects()\Width And 
       Y >= Objects()\Y And Y <= Objects()\Y + Objects()\Height
      ProcedureReturn idx
    ENDIF
    idx - 1
  Until Not PreviousElement(Objects())
  
  ProcedureReturn -1
EndProcedure

Procedure.s CheckResizeHandle(X.l, Y.l, ObjIdx.i)
  SelectElement(Objects(),ObjIdx)

  IF Objects()\isSelected = 1
    IF X >= Objects()\X + Objects()\Width - #RESIZE_HANDLE And 
       X <= Objects()\X + Objects()\Width And
       Y >= Objects()\Y + Objects()\Height - #RESIZE_HANDLE And
       Y <= Objects()\Y + Objects()\Height
      ProcedureReturn "SE"
    ENDIF
  ENDIF

  ProcedureReturn ""
EndProcedure

Procedure RedrawCanvas()
  DrawGrid()
  DrawObjects()
  
  IF StartDrawing(CanvasOutput(#Canvas_Designer))
    DrawImage(ImageID(OffscreenImage), 0, 0)
    StopDrawing()
  ENDIF
EndProcedure

Procedure SaveFile(bFull)
   File$ = SaveFileRequester("Save Design (Full)", "design.txt", "Text Files (*.txt)|*.txt", 0)
   IF File$
      IF CreateFile(0, File$)
         IF bFull
            WriteStringN(0, "COUNTER_SECTION")
            WriteStringN(0, "BUTTON_COUNTER=" + Str(ButtonCounter))
            WriteStringN(0, "LABEL_COUNTER=" + Str(LabelCounter))
            WriteStringN(0, "EDIT_COUNTER=" + Str(EditCounter))
            WriteStringN(0, "PHOTO_COUNTER=" + Str(PhotoCounter))
            WriteStringN(0, "END_COUNTER_SECTION")
            WriteStringN(0, "")
         ENDIF
         ForEach Objects()
            WriteStringN(0, "TYPE=" + Objects()\Type)
            WriteStringN(0, "X=" + Str(Objects()\X))
            WriteStringN(0, "Y=" + Str(Objects()\Y))
            WriteStringN(0, "WIDTH=" + Str(Objects()\Width))
            WriteStringN(0, "HEIGHT=" + Str(Objects()\Height))
            WriteStringN(0, "TEXT=" + Objects()\Text)
            WriteStringN(0, "IMAGEPATH=" + Objects()\ImagePath)
            WriteStringN(0, "---")
         Next
         CloseFile(0)
         MessageRequester("Success", "Full design saved!", #PB_MessageRequester_Info)
      ENDIF
   ENDIF
   
EndProcedure

Procedure LoadDesign()
  File$ = OpenFileRequester("Load Design", "", "Text Files (*.txt)|*.txt", 0)
  IF File$ And FileSize(File$) > 0
    ClearList(Objects())
    ClearSelection()
    
    ButtonCounter = 0
    LabelCounter = 0
    EditCounter = 0
    PhotoCounter = 0
    
    IF ReadFile(0, File$)
      PROTECTED InCounterSection.i = #False
      
      While Not Eof(0)
        Line$ = ReadString(0)
        
        IF Line$ = "COUNTER_SECTION"
          InCounterSection = #True
          Continue
        ELSEIF Line$ = "END_COUNTER_SECTION"
          InCounterSection = #False
          Continue
        ENDIF
        
        IF InCounterSection
          IF Left(Line$, 15) = "BUTTON_COUNTER="
            ButtonCounter = Val(Mid(Line$, 16))
          ELSEIF Left(Line$, 14) = "LABEL_COUNTER="
            LabelCounter = Val(Mid(Line$, 15))
          ELSEIF Left(Line$, 13) = "EDIT_COUNTER="
            EditCounter = Val(Mid(Line$, 14))
          ELSEIF Left(Line$, 14) = "PHOTO_COUNTER="
            PhotoCounter = Val(Mid(Line$, 15))
          ENDIF
          Continue
        ENDIF
        
        IF Line$ = "---"
          Continue
        ENDIF
        
        IF Left(Line$, 5) = "TYPE="
          AddElement(Objects())
          Objects()\Type = Mid(Line$, 6)
        ELSEIF Left(Line$, 2) = "X="
          Objects()\X = Val(Mid(Line$, 3))
        ELSEIF Left(Line$, 2) = "Y="
          Objects()\Y = Val(Mid(Line$, 3))
        ELSEIF Left(Line$, 6) = "WIDTH="
          Objects()\Width = Val(Mid(Line$, 7))
        ELSEIF Left(Line$, 7) = "HEIGHT="
          Objects()\Height = Val(Mid(Line$, 8))
        ELSEIF Left(Line$, 5) = "TEXT="
          Objects()\Text = Mid(Line$, 6)
        ELSEIF Left(Line$, 10) = "IMAGEPATH="
          Objects()\ImagePath = Mid(Line$, 11)
        ENDIF
      Wend
      CloseFile(0)
      
      RedrawCanvas()
      MessageRequester("Success", "Design loaded!", #PB_MessageRequester_Info)
    ENDIF
  ENDIF
EndProcedure

Procedure DeleteSelected()
    PROTECTED isDelete.l = 0
    ForEach Objects()
       IF Objects()\isSelected = 1
          DeleteElement(Objects())
          isDelete = 1
       ENDIF       
    Next
    
    IF isDelete > 0
       RedrawCanvas()
    ENDIF
EndProcedure

Procedure AlignLeft()
   PROTECTED MinX.l = 999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         IF Objects()\X < MinX
            MinX = Objects()\X
         ENDIF
      ENDIF
   Next
   
   IF MinX <> 999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\X = MinX
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignRight()
   PROTECTED MaxRight.l = -999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         PROTECTED RightEdge.l = Objects()\X + Objects()\Width
         IF RightEdge > MaxRight
            MaxRight = RightEdge
         ENDIF
      ENDIF
   Next
   
   IF MaxRight <> -999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\X = MaxRight - Objects()\Width
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignTop()
   PROTECTED MinY.l = 999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         IF Objects()\Y < MinY
            MinY = Objects()\Y
         ENDIF
      ENDIF
   Next
   
   IF MinY <> 999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\Y = MinY
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignBottom()
   PROTECTED MaxBottom.l = -999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         PROTECTED BottomEdge.l = Objects()\Y + Objects()\Height
         IF BottomEdge > MaxBottom
            MaxBottom = BottomEdge
         ENDIF
      ENDIF
   Next
   
   IF MaxBottom <> -999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\Y = MaxBottom - Objects()\Height
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure MatchSize()
   IF CheckTotalSelected() < 2
      ProcedureReturn
   ENDIF
   
   PROTECTED FirstIdx.l = GetFirstSelectedIndex()
   IF FirstIdx < 0
      ProcedureReturn
   ENDIF
   
   SelectElement(Objects(), FirstIdx)
   PROTECTED TargetWidth.l = Objects()\Width
   PROTECTED TargetHeight.l = Objects()\Height
   
   ForEach Objects()
      IF Objects()\isSelected = 1 And ListIndex(Objects()) <> FirstIdx
         Objects()\Width = TargetWidth
         Objects()\Height = TargetHeight
      ENDIF
   Next
   
   RedrawCanvas()
EndProcedure

Procedure DistributeVertical()
   SpacingStr$ = GetGadgetText(#String_Spacing)
   PROTECTED CustomSpacing.l = Val(SpacingStr$)
   IF CustomSpacing < 0 : CustomSpacing = 5 : ENDIF
   
   NewList YPositions.FormObject()
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         AddElement(YPositions())
         YPositions()\Y = Objects()\Y
         YPositions()\Height = Objects()\Height
         YPositions()\X = ListIndex(Objects())
      ENDIF
   Next
   
   SortStructuredList(YPositions(), #PB_Sort_Ascending, OffsetOf(FormObject\Y), TypeOf(FormObject\Y))
   
   FirstElement(YPositions())
   PROTECTED CurrentY.l = YPositions()\Y
   
   ForEach YPositions()
      PROTECTED ObjIdx.l = YPositions()\X
      SelectElement(Objects(), ObjIdx)
      Objects()\Y = SnapToGrid(CurrentY)
      CurrentY = Objects()\Y + Objects()\Height + CustomSpacing
   Next
   
   RedrawCanvas()
EndProcedure

Procedure RenumberObjects()
   PROTECTED BtnNum.l = 0
   PROTECTED LblNum.l = 0
   PROTECTED EdtNum.l = 0
   PROTECTED PhtNum.l = 0
   
   ForEach Objects()
      SELECT Objects()\Type
         CASE "BTN"
            BtnNum + 1
            Objects()\Text = "Button " + RSet(Str(BtnNum), 2, "0")
         CASE "LABEL"
            LblNum + 1
            Objects()\Text = "Label " + RSet(Str(LblNum), 2, "0")
         CASE "EDIT"
            EdtNum + 1
            Objects()\Text = "Edit " + RSet(Str(EdtNum), 2, "0")
         CASE "PHOTO"
            PhtNum + 1
            Objects()\Text = "Photo " + RSet(Str(PhtNum), 2, "0")
      ENDSELECT
   Next
   
   ButtonCounter = BtnNum
   LabelCounter = LblNum
   EditCounter = EdtNum
   PhotoCounter = PhtNum
   
   RedrawCanvas()
EndProcedure

Procedure NewDesign()
  Result.i = MessageRequester("New Design", "Clear all objects and start new?", #PB_MessageRequester_YesNo)
  IF Result = #PB_MessageRequester_Yes
    ClearList(Objects())
    ClearSelection()
    ButtonCounter = 0
    LabelCounter = 0
    EditCounter = 0
    PhotoCounter = 0
    RedrawCanvas()
  ENDIF
EndProcedure

OpenWindow(#Win_Main, 0, 0, 900, 650, "My DB Form Designer "+#Version$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered | #PB_Window_SizeGadget | #PB_Window_MaximizeGadget)

CanvasGadget(#Canvas_Designer, 10, 10, 700, 550)

TextGadget(#LABEL_OBJECT, 720, 10, 160, 20, "Object:")
ButtonGadget(#Btn_AddButton, 720, 30, 160, 28, "Add Button")
ButtonGadget(#Btn_AddLabel, 720, 65, 160, 28, "Add Label")
ButtonGadget(#Btn_AddEdit, 720, 100, 160, 28, "Add Edit")
ButtonGadget(#Btn_AddPhoto, 720, 135, 160, 28, "Add Photo")
ButtonGadget(#Btn_Delete, 720, 170, 160, 28, "Delete Selected")

TextGadget(#LABEL_ALIGNMENT, 720, 210, 160, 20, "Alignment:")
ButtonGadget(#Btn_AlignLeft, 720, 235, 75, 25, "Left")
ButtonGadget(#Btn_AlignRight, 805, 235, 75, 25, "Right")
ButtonGadget(#Btn_AlignTop, 720, 265, 75, 25, "Top")
ButtonGadget(#Btn_AlignBottom, 805, 265, 75, 25, "Bottom")
ButtonGadget(#Btn_MatchSize, 720, 295, 160, 25, "Match Size (1st)")
ButtonGadget(#Btn_DistributeV, 720, 325, 160, 25, "Distribute Vertical")
ButtonGadget(#Btn_Renumber, 720, 355, 160, 25, "Renumber Objects")

TextGadget(#Text_Spacing, 720, 390, 100, 20, "V-Spacing (px):")
StringGadget(#String_Spacing, 820, 388, 60, 22, "5")

TextGadget(#LABEL_FILE, 720, 420, 160, 20, "File:")
ButtonGadget(#Btn_New, 720, 440, 160, 28, "New Design")
ButtonGadget(#Btn_Save, 720, 475, 160, 28, "Save Design (Full)")
ButtonGadget(#Btn_SaveClean, 720, 510, 160, 28, "Save Design (Clean)")
ButtonGadget(#Btn_Load, 720, 545, 160, 28, "Load Design")

TextGadget(#Text_Info, 10, 570, 700, 60, "Ctrl+Click to multi-select. Drag to move. Drag handle to resize." + #LF$ + "Selection confirmed on mouse release. Objects snap to 5px grid.")

OffscreenImage = CreateImage(#PB_Any, 700, 550)

DrawGrid()

Procedure ResizeControls()
  PROTECTED WinW.l = WindowWidth(#Win_Main)
  PROTECTED WinH.l = WindowHeight(#Win_Main)
  PROTECTED CanvasW.l = WinW - 190
  PROTECTED CanvasH.l = WinH - 90
  PROTECTED InfoY.l = WinH - 70
  PROTECTED ButtonX.l = WinW - 170
  
  IF CanvasW < 400 : CanvasW = 400 : ENDIF
  IF CanvasH < 300 : CanvasH = 300 : ENDIF
  
  ResizeGadget(#Canvas_Designer, 10, 10, CanvasW, CanvasH)
  ResizeGadget(#LABEL_OBJECT, ButtonX, 10, 160, 20)
  ResizeGadget(#Btn_AddButton, ButtonX, 30, 160, 27)
  ResizeGadget(#Btn_AddLabel, ButtonX, 65, 160, 27)
  ResizeGadget(#Btn_AddEdit, ButtonX, 100, 160, 27)
  ResizeGadget(#Btn_AddPhoto, ButtonX, 135, 160, 27)
  ResizeGadget(#Btn_Delete, ButtonX, 170, 160, 27)
  ResizeGadget(#LABEL_ALIGNMENT, ButtonX, 210, 160, 20)
  ResizeGadget(#Btn_AlignLeft, ButtonX, 235, 75, 25)
  ResizeGadget(#Btn_AlignRight, ButtonX + 85, 235, 75, 25)
  ResizeGadget(#Btn_AlignTop, ButtonX, 265, 75, 25)
  ResizeGadget(#Btn_AlignBottom, ButtonX + 85, 265, 75, 25)
  ResizeGadget(#Btn_MatchSize, ButtonX, 295, 160, 25)
  ResizeGadget(#Btn_DistributeV, ButtonX, 325, 160, 25)
  ResizeGadget(#Btn_Renumber, ButtonX, 355, 160, 25)
  ResizeGadget(#Text_Spacing, ButtonX, 390, 100, 20)
  ResizeGadget(#String_Spacing, ButtonX + 100, 388, 60, 22)
  ResizeGadget(#LABEL_FILE, ButtonX, 420, 160, 20)
  ResizeGadget(#Btn_New, ButtonX, 440, 160, 27)
  ResizeGadget(#Btn_Save, ButtonX, 475, 160, 27)
  ResizeGadget(#Btn_SaveClean, ButtonX, 510, 160, 27)
  ResizeGadget(#Btn_Load, ButtonX, 545, 160, 27)
  ResizeGadget(#Text_Info, 10, InfoY, CanvasW, 60)
  
  IF IsImage(OffscreenImage)
    FreeImage(OffscreenImage)
  ENDIF
  OffscreenImage = CreateImage(#PB_Any, CanvasW, CanvasH)
  
  RedrawCanvas()
EndProcedure

Repeat
  Event = WaitWindowEvent()
  
  SELECT Event
    CASE #PB_Event_SizeWindow
      ResizeControls()
      
    CASE #PB_Event_Gadget
      SELECT EventGadget()
        CASE #Btn_AddButton
          AddObject("BTN")
          RedrawCanvas()
        CASE #Btn_AddLabel
          AddObject("LABEL")
          RedrawCanvas()
        CASE #Btn_AddEdit
          AddObject("EDIT")
          RedrawCanvas()
        CASE #Btn_AddPhoto
          AddObject("PHOTO")
          RedrawCanvas()
        CASE #Btn_Delete : DeleteSelected()
        CASE #Btn_AlignLeft : AlignLeft()
        CASE #Btn_AlignRight : AlignRight()
        CASE #Btn_AlignTop : AlignTop()
        CASE #Btn_AlignBottom : AlignBottom()
        CASE #Btn_MatchSize : MatchSize()
        CASE #Btn_DistributeV : DistributeVertical()
        CASE #Btn_Renumber : RenumberObjects()
        CASE #Btn_New : NewDesign()
        CASE #Btn_Save : SaveFile(1)
        CASE #Btn_SaveClean : SaveFile(0)
        CASE #Btn_Load : LoadDesign()
          
        CASE #Canvas_Designer
          SELECT EventType()
            CASE #PB_EventType_LeftButtonDown
              X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
              Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
              ClickedObj.i = FindObjectAt(X, Y)
              HasMoved = #False
              
              PendingClickObj = ClickedObj
              PendingCtrlPressed = #False
              IF GetAsyncKeyState_(#VK_CONTROL) & $8000
                PendingCtrlPressed = #True
              ENDIF
              
              IF ClickedObj >= 0
                IF CheckTotalSelected() = 1 And IsSelected(ClickedObj)
                  ResizeEdge = CheckResizeHandle(X, Y, ClickedObj)
                ELSE
                  ResizeEdge = ""
                ENDIF
                
                IF ResizeEdge <> ""
                  ResizeMode = #True
                  DragStartX = X
                  DragStartY = Y
                  SelectElement(Objects(), ClickedObj)
                  DragObjX = Objects()\X
                  DragObjY = Objects()\Y
                  DragObjW = Objects()\Width
                  DragObjH = Objects()\Height
                ELSE
                  IF IsSelected(ClickedObj)
                    DragMode = #True
                    DragStartX = X
                    DragStartY = Y
                  ENDIF
                ENDIF
              ENDIF
              
            CASE #PB_EventType_LeftButtonUp
              IF PendingClickObj >= 0 And Not HasMoved And Not ResizeMode
                IF PendingCtrlPressed
                  IF IsSelected(PendingClickObj)
                    RemoveSelection(PendingClickObj)
                  ELSE
                    AddSelection(PendingClickObj)
                  ENDIF
                ELSE
                  ClearSelection()
                  AddSelection(PendingClickObj)
                ENDIF
                RedrawCanvas()
              ELSEIF PendingClickObj < 0 And Not HasMoved And Not PendingCtrlPressed
                ClearSelection()
                RedrawCanvas()
              ENDIF
              
              DragMode = #False
              ResizeMode = #False
              ResizeEdge = ""
              PendingClickObj = -1
              HasMoved = #False
              
            CASE #PB_EventType_MouseMove
              IF ResizeMode And CheckTotalSelected() = 1
                HasMoved = #True
                X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
                Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
                
                DX = X - DragStartX
                DY = Y - DragStartY
                
                ForEach Objects()
                  IF Objects()\isSelected
                    NewW.l = DragObjW + DX
                    NewH.l = DragObjH + DY
                    
                    IF NewW < 20 : NewW = 20 : ENDIF
                    IF NewH < 20 : NewH = 20 : ENDIF
                    
                    Objects()\Width = SnapToGrid(NewW)
                    Objects()\Height = SnapToGrid(NewH)
                    
                    RedrawCanvas()
                    Break
                  ENDIF
                Next
                
              ELSEIF DragMode And CheckTotalSelected() > 0
                HasMoved = #True
                X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
                Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
                
                DX = X - DragStartX
                DY = Y - DragStartY
                
                ForEach Objects()
                  IF Objects()\isSelected
                    Objects()\X = SnapToGrid(Objects()\X + DX)
                    Objects()\Y = SnapToGrid(Objects()\Y + DY)
                    
                    IF Objects()\X < 0 : Objects()\X = 0 : ENDIF
                    IF Objects()\Y < 0 : Objects()\Y = 0 : ENDIF
                  ENDIF
                Next
                
                DragStartX = X
                DragStartY = Y
                RedrawCanvas()
              ENDIF
          ENDSELECT
      ENDSELECT
      
    CASE #PB_Event_CloseWindow
      Break
  ENDSELECT
ForEver

End                    

Last edited by moricode on Sat Jan 10, 2026 7:37 am, edited 7 times in total.
User avatar
Nudgy
User
User
Posts: 27
Joined: Mon May 27, 2024 8:11 pm

Re: Simple and useful form designer

Post by Nudgy »

Thanks for sharing. This is a nice example.

Only one minor issue I noticed: The following function does not work and I cannot find it in the documentation: GetAsyncKeyState_

If the conditional is commented out, the application compiles.
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5604
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: Simple and useful form designer

Post by Kwai chang caine »

Perhaps you have a demo version of PB :wink:
GetAsyncKeyState_() is an API window
Like all the function who finish with the underscore _
With demo version i believe you cannot use them
ImageThe happiness is a road...
Not a destination

PureBasic French Forum
User avatar
Nudgy
User
User
Posts: 27
Joined: Mon May 27, 2024 8:11 pm

Re: Simple and useful form designer

Post by Nudgy »

I have the full version :D

But I use Linux. Maybe GetAsyncKeyState_() refers to Win API?
moricode
Enthusiast
Enthusiast
Posts: 167
Joined: Thu May 25, 2023 3:55 am

Re: Simple and useful form designer

Post by moricode »

Nudgy wrote: Fri Jan 09, 2026 11:31 am I have the full version :D

But I use Linux. Maybe GetAsyncKeyState_() refers to Win API?
yes , it is windows api , not linux ..
Axolotl
Addict
Addict
Posts: 913
Joined: Wed Dec 31, 2008 3:36 pm

Re: Simple and useful form designer

Post by Axolotl »

Thanks for sharing.
Some thought (if you dont mind)
Hint: I recommend to use .i instead of .l (advantage: you don't need to write that (only in structures))

Observation: When moving objects, it is very jerky.

Suggestion:
Since GetAsyncKeyState_() is the only WinAPI function, it can be easily customized.

Code: Select all

CanvasGadget(#Canvas_Designer, 10, 10, 700, 550, #PB_Canvas_Keyboard)  ; add Keyboard flag 
; .....
        Case #Canvas_Designer
          Select EventType()
            Case #PB_EventType_KeyDown ; add this event handling 
              If GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Key) & #VK_CONTROL 
                PendingCtrlPressed = #True 
              EndIf

            Case #PB_EventType_KeyUp ; add this event handling 
              If GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Key) & #VK_CONTROL 
                PendingCtrlPressed = #False 
              EndIf
; .....
            Case #PB_EventType_LeftButtonDown
; .....
; remove/comment the following lines 
;               PendingCtrlPressed = #False 
;               If GetAsyncKeyState_(#VK_CONTROL) & $8000
;                 PendingCtrlPressed = #True : Debug "Control Key pressed. "
;               EndIf
; .....
A Bug fix for Error on click in an empty canvas (empty list): (see below my changes)

Code: Select all

Procedure FindObjectAt(X.l, Y.l)
  idx = ListSize(Objects()) - 1
  LastElement(Objects())
  
  If idx > -1   ; Axolotl -> check for empty list 
  Repeat
    If X >= Objects()\X And X <= Objects()\X + Objects()\Width And 
       Y >= Objects()\Y And Y <= Objects()\Y + Objects()\Height
      ProcedureReturn idx
    EndIf
    idx - 1
  Until Not PreviousElement(Objects())
  EndIf ; Axolotl 
  
  ProcedureReturn -1
EndProcedure
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
moricode
Enthusiast
Enthusiast
Posts: 167
Joined: Thu May 25, 2023 3:55 am

Re: Simple and useful form designer

Post by moricode »

Now Upgrade to the new advance version 1.10 :

I use this tools to build DBase IV / SQLite Record View/Edit forms , save the design layout, load the layout position in your database program and read the SQLite table and put in the layout field, it is very convenience . before this , i was hardcode the record fields position by hand .


Feature added :
1. Added selection box for easy multiple object selection
2. Added [Select ALL],[Select Label], [Select Edit] Button for fast selection
3. use proper mouse icon when mouse hover over Selected Objects/ Object Resize Corner
4. Fixed new object placement position , now it will not overlaps or cover other objects when add new
5. smart object placement start from vertical direction first then to next horizontal column if reach canvas bottom
6. Fixed to smooth object movement , no more jerky step


Updated Version 1.10 :

Code: Select all

; Visual Form Designer in PureBasic 5.7x , 6.1x , 6.2x
; Features: BTN, LABEL, EDIT, PHOTO objects with 5px grid snapping
; Enhanced: Dynamic resize, Move cursor, Selection box

#Version$ = "1.1 pro"
Structure FormObject
  Type.s
  X.l
  Y.l
  Width.l
  Height.l
  Text.s
  ImagePath.s
  isSelected.l
  SelectionOrder.l
EndStructure

#GRID_SIZE = 5
#MAX_OBJECTS = 100

Global NewList Objects.FormObject()
Global DragMode.i = #False
Global ResizeMode.i = #False
Global ResizeEdge.s = ""
Global DragStartX.l, DragStartY.l
Global DragObjX.l, DragObjY.l
Global DragObjW.l, DragObjH.l
Global OffscreenImage.i
Global SelectionCounter.l = 0
Global PendingClickObj.i = -1
Global PendingCtrlPressed.i = #False
Global HasMoved.i = #False
Global ButtonCounter.l = 0
Global LabelCounter.l = 0
Global EditCounter.l = 0
Global PhotoCounter.l = 0

; Selection box variables
Global SelectionBoxActive.i = #False
Global SelectionBoxStartX.l, SelectionBoxStartY.l
Global SelectionBoxEndX.l, SelectionBoxEndY.l

; Store original positions/sizes for dynamic resize
Structure ObjectSnapshot
  X.l
  Y.l
  Width.l
  Height.l
EndStructure
Global NewList SnapShots.ObjectSnapshot()

#RESIZE_HANDLE = 8

Enumeration
  #Win_Main
  #Canvas_Designer  
  #Text_Info
  #Btn_AddButton
  #Btn_AddLabel
  #Btn_AddEdit
  #Btn_AddPhoto
  #Btn_Delete
  #Btn_Save
  #Btn_SaveClean
  #Btn_Load
  #Btn_New
  #Btn_AlignLeft
  #Btn_AlignRight
  #Btn_AlignTop
  #Btn_AlignBottom
  #Btn_DistributeV
  #Btn_MatchSize
  #Btn_Renumber
  #Text_Spacing
  #String_Spacing
  #LABEL_OBJECT
  #LABEL_ALIGNMENT
  #LABEL_FILE  
  #Btn_SelectALL
  #Btn_SelectLabel
  #Btn_SelectEDIT
EndEnumeration

Procedure SnapToGrid(Value.l)
  ProcedureReturn Round(Value / #GRID_SIZE, #PB_Round_Nearest) * #GRID_SIZE
EndProcedure

Procedure IsSelected(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
    IF Objects()\isSelected = 1
      ProcedureReturn #True
    ENDIF
  ProcedureReturn #False
EndProcedure

Procedure CheckTotalSelected()
   PROTECTED Total.l = 0
   ForEach Objects()
      IF Objects()\isSelected
         Total = Total + 1
      ENDIF
   Next
   ProcedureReturn Total
EndProcedure

Procedure CheckSelected()
   ForEach Objects()
      IF Objects()\isSelected
         ProcedureReturn ListIndex(Objects())
      ENDIF                      
   Next
   ProcedureReturn -1
EndProcedure

Procedure GetFirstSelectedIndex()
   PROTECTED MinOrder.l = 999999
   PROTECTED FirstIdx.l = -1
   PROTECTED idx.l = 0
   
   ForEach Objects()
      IF Objects()\isSelected
         IF Objects()\SelectionOrder < MinOrder
            MinOrder = Objects()\SelectionOrder
            FirstIdx = idx
         ENDIF
      ENDIF
      idx + 1
   Next
   ProcedureReturn FirstIdx
EndProcedure

Procedure AddSelection(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   Objects()\isSelected = 1
   SelectionCounter + 1
   Objects()\SelectionOrder = SelectionCounter
EndProcedure

Procedure RemoveSelection(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   Objects()\isSelected = 0
   Objects()\SelectionOrder = 0
EndProcedure

Procedure ClearSelection()
   ForEach Objects()
      Objects()\isSelected = 0
      Objects()\SelectionOrder = 0
   Next
   SelectionCounter = 0
EndProcedure

Procedure SaveSnapshots()
   ClearList(SnapShots())
   ForEach Objects()
      IF Objects()\isSelected
         AddElement(SnapShots())
         SnapShots()\X = Objects()\X
         SnapShots()\Y = Objects()\Y
         SnapShots()\Width = Objects()\Width
         SnapShots()\Height = Objects()\Height
      ENDIF
   Next
EndProcedure

Procedure DrawGrid()
  IF StartDrawing(ImageOutput(OffscreenImage))
    DrawingMode(#PB_2DDrawing_Default)
    Box(0, 0, OutputWidth(), OutputHeight(), RGB(255, 255, 255))
    
    For x.l = 0 To OutputWidth() Step #GRID_SIZE
      For y.l = 0 To OutputHeight() Step #GRID_SIZE
        LineXY(x, y, x, y, RGB(220, 220, 220))
      Next
    Next
    StopDrawing()
  ENDIF
EndProcedure

Procedure DrawObjects()
   IF StartDrawing(ImageOutput(OffscreenImage))
      ForEach Objects()
         SELECT Objects()\Type
            CASE "BTN"
               DrawingMode(#PB_2DDrawing_Default)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(240, 240, 240))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(0, 0, 0))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X + 5, Objects()\Y + 5, Objects()\Text, RGB(0, 0, 0))
            CASE "LABEL"
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X, Objects()\Y, Objects()\Text, RGB(0, 0, 0))               
            CASE "EDIT"
               DrawingMode(#PB_2DDrawing_Default)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(255, 255, 255))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(128, 128, 128))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X + 3, Objects()\Y + 3, Objects()\Text, RGB(128, 128, 128))               
            CASE "PHOTO"
               DrawingMode(#PB_2DDrawing_Default)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(200, 200, 200))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(Objects()\X, Objects()\Y, Objects()\Width, Objects()\Height, RGB(0, 0, 0))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(Objects()\X + Objects()\Width/2 - 25, Objects()\Y + Objects()\Height/2 - 5, Objects()\Text, RGB(100, 100, 100))               
         ENDSELECT
         IF Objects()\isSelected  
            DrawingMode(#PB_2DDrawing_Outlined)
            Box(Objects()\X - 2, Objects()\Y - 2, Objects()\Width + 4, Objects()\Height + 4, RGB(0, 120, 215))
            DrawingMode(#PB_2DDrawing_Default)
            Box(Objects()\X + Objects()\Width - #RESIZE_HANDLE, Objects()\Y + Objects()\Height - #RESIZE_HANDLE, #RESIZE_HANDLE, #RESIZE_HANDLE, RGB(0, 120, 215))
            Box(Objects()\X + Objects()\Width - #RESIZE_HANDLE + 1, Objects()\Y + Objects()\Height - #RESIZE_HANDLE + 1, #RESIZE_HANDLE - 2, #RESIZE_HANDLE - 2, RGB(255, 255, 255))   
         ENDIF    
      Next
      
      ; Draw selection box
      IF SelectionBoxActive
         DrawingMode(#PB_2DDrawing_Outlined | #PB_2DDrawing_AlphaBlend)
         PROTECTED BoxX.l = Min(SelectionBoxStartX, SelectionBoxEndX)
         PROTECTED BoxY.l = Min(SelectionBoxStartY, SelectionBoxEndY)
         PROTECTED BoxW.l = Abs(SelectionBoxEndX - SelectionBoxStartX)
         PROTECTED BoxH.l = Abs(SelectionBoxEndY - SelectionBoxStartY)
         Box(BoxX, BoxY, BoxW, BoxH, RGBA(0, 120, 215, 80))
         DrawingMode(#PB_2DDrawing_Outlined)
         Box(BoxX, BoxY, BoxW, BoxH, RGB(0, 120, 215))
      ENDIF
      
      StopDrawing()
   ENDIF
EndProcedure

Procedure FindEmptySpace(*Width.Long, *Height.Long)
   ; input the W,H , but output the found X,Y
  PROTECTED TestX.l, TestY.l
  PROTECTED Found.i
  PROTECTED CanvasH.l = GadgetHeight(#Canvas_Designer)
  PROTECTED CanvasW.l = GadgetWidth(#Canvas_Designer)
  PROTECTED ObjWidth.l = *Width\l
  PROTECTED ObjHeight.l = *Height\l
  
  ; object always search from 5,5, and then check overlaps every 5 pixel vertical 
  ; and every 20 pixel horizontal for the best position
  
  For TestX = 5 To CanvasW - ObjWidth Step 20
     For TestY = 5 To CanvasH - ObjHeight Step 5
        Found = #True
        
        ; Check for overlap with existing objects
        IF ListSize(Objects()) > 0
           ForEach Objects()
              IF Not (TestX + ObjWidth <= Objects()\X Or TestX >= Objects()\X + Objects()\Width Or
                      TestY + ObjHeight <= Objects()\Y Or TestY >= Objects()\Y + Objects()\Height)
                 ; Overlap detected
                 Found = #False
                 Break
              ENDIF
           Next
        ENDIF
        
        IF Found
           *Width\l = TestX
           *Height\l = TestY
           ProcedureReturn #True
        ENDIF
     Next
  Next
  
  ; If no space found, return default position
  *Width\l = 5
  *Height\l = 5
  ProcedureReturn #False
EndProcedure

Procedure AddObject(Type.s)
  PROTECTED NewX.l, NewY.l
  PROTECTED TempW.l, TempH.l
  
  
  SELECT Type
    CASE "BTN"
      TempW = 100
      TempH = 22
      ButtonCounter + 1
      T$ = "Button " + RSet(Str(ButtonCounter), 2, "0")
    CASE "LABEL"
      TempW = 60
      TempH = 20
      LabelCounter + 1
      T$ = "Label " + RSet(Str(LabelCounter), 2, "0")
    CASE "EDIT"
      TempW = 150
      TempH = 20
      EditCounter + 1
      T$ = "Edit " + RSet(Str(EditCounter), 2, "0")
    CASE "PHOTO"
      TempW = 100
      TempH = 100
      PhotoCounter + 1
      T$ = "Photo " + RSet(Str(PhotoCounter), 2, "0")
  ENDSELECT
  
  
  NewX = TempW
  NewY = TempH
  FindEmptySpace(@NewX, @NewY)
  AddElement(Objects())
  Objects()\Type = Type
  Objects()\X = NewX
  Objects()\Y = NewY
  Objects()\Text = T$
  Objects()\Width = TempW
  Objects()\Height = TempH
  
  ClearSelection()
  AddSelection(ListSize(Objects()) - 1)
EndProcedure

Procedure FindObjectAt(X.l, Y.l)
  PROTECTED idx.l
  
  IF ListSize(Objects()) = 0
    ProcedureReturn -1
 ENDIF
 
  idx = ListSize(Objects()) - 1
  LastElement(Objects())
  
  Repeat
    IF X >= Objects()\X And X <= Objects()\X + Objects()\Width And 
       Y >= Objects()\Y And Y <= Objects()\Y + Objects()\Height
      ProcedureReturn idx
    ENDIF
    idx - 1
  Until Not PreviousElement(Objects())
  
  ProcedureReturn -1
EndProcedure

Procedure.s CheckResizeHandle(X.l, Y.l, ObjIdx.i)
  SelectElement(Objects(),ObjIdx)

  IF Objects()\isSelected = 1
    IF X >= Objects()\X + Objects()\Width - #RESIZE_HANDLE And 
       X <= Objects()\X + Objects()\Width And
       Y >= Objects()\Y + Objects()\Height - #RESIZE_HANDLE And
       Y <= Objects()\Y + Objects()\Height
      ProcedureReturn "SE"
    ENDIF
  ENDIF

  ProcedureReturn ""
EndProcedure

Procedure UpdateMouseCursor(X.l, Y.l)
  PROTECTED Obj.i = FindObjectAt(X, Y)
  
  IF Obj >= 0
    IF CheckTotalSelected() = 1 And IsSelected(Obj)
      PROTECTED Edge.s = CheckResizeHandle(X, Y, Obj)
      IF Edge = "SE"
        SetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Cursor, #PB_Cursor_LeftUpRightDown)
        ProcedureReturn
      ENDIF
    ENDIF
    
    IF IsSelected(Obj)
      SetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Cursor,#PB_Cursor_Arrows); #PB_Cursor_Cross)
      ProcedureReturn
    ENDIF
  ENDIF
  
  SetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Cursor, #PB_Cursor_Default)
EndProcedure

Procedure SelectObjectsInBox()
   PROTECTED BoxX.l = Min(SelectionBoxStartX, SelectionBoxEndX)
   PROTECTED BoxY.l = Min(SelectionBoxStartY, SelectionBoxEndY)
   PROTECTED BoxW.l = Abs(SelectionBoxEndX - SelectionBoxStartX)
   PROTECTED BoxH.l = Abs(SelectionBoxEndY - SelectionBoxStartY)
   PROTECTED BoxRight.l = BoxX + BoxW
   PROTECTED BoxBottom.l = BoxY + BoxH
   
   ForEach Objects()
      PROTECTED ObjRight.l = Objects()\X + Objects()\Width
      PROTECTED ObjBottom.l = Objects()\Y + Objects()\Height
      
      IF Not (Objects()\X > BoxRight Or ObjRight < BoxX Or 
              Objects()\Y > BoxBottom Or ObjBottom < BoxY)
         IF Not Objects()\isSelected
            AddSelection(ListIndex(Objects()))
         ENDIF
      ENDIF
   Next
EndProcedure

Procedure RedrawCanvas()
  DrawGrid()
  DrawObjects()
  
  IF StartDrawing(CanvasOutput(#Canvas_Designer))
    DrawImage(ImageID(OffscreenImage), 0, 0)
    StopDrawing()
  ENDIF
EndProcedure

Procedure SaveFile(bFull)
   File$ = SaveFileRequester("Save Design", "design.txt", "Text Files (*.txt)|*.txt", 0)
   IF File$
      IF CreateFile(0, File$)
         IF bFull
            WriteStringN(0, "COUNTER_SECTION")
            WriteStringN(0, "BUTTON_COUNTER=" + Str(ButtonCounter))
            WriteStringN(0, "LABEL_COUNTER=" + Str(LabelCounter))
            WriteStringN(0, "EDIT_COUNTER=" + Str(EditCounter))
            WriteStringN(0, "PHOTO_COUNTER=" + Str(PhotoCounter))
            WriteStringN(0, "END_COUNTER_SECTION")
            WriteStringN(0, "")
         ENDIF
         ForEach Objects()
            WriteStringN(0, "TYPE=" + Objects()\Type)
            WriteStringN(0, "X=" + Str(Objects()\X))
            WriteStringN(0, "Y=" + Str(Objects()\Y))
            WriteStringN(0, "WIDTH=" + Str(Objects()\Width))
            WriteStringN(0, "HEIGHT=" + Str(Objects()\Height))
            IF bFull = 1
            WriteStringN(0, "TEXT=" + Objects()\Text)
            WriteStringN(0, "---")
            ENDIF
         Next
         CloseFile(0)
         MessageRequester("Success", "Design saved!", #PB_MessageRequester_Info)
      ENDIF
   ENDIF
EndProcedure

Procedure LoadDesign()
  File$ = OpenFileRequester("Load Design", "", "Text Files (*.txt)|*.txt", 0)
  IF File$ And FileSize(File$) > 0
    ClearList(Objects())
    ClearSelection()
    
    ButtonCounter = 0
    LabelCounter = 0
    EditCounter = 0
    PhotoCounter = 0
    
    IF ReadFile(0, File$)
      PROTECTED InCounterSection.i = #False
      
      While Not Eof(0)
        Line$ = ReadString(0)
        
        IF Line$ = "COUNTER_SECTION"
          InCounterSection = #True
          Continue
        ELSEIF Line$ = "END_COUNTER_SECTION"
          InCounterSection = #False
          Continue
        ENDIF
        
        IF InCounterSection
          IF Left(Line$, 15) = "BUTTON_COUNTER="
            ButtonCounter = Val(Mid(Line$, 16))
          ELSEIF Left(Line$, 14) = "LABEL_COUNTER="
            LabelCounter = Val(Mid(Line$, 15))
          ELSEIF Left(Line$, 13) = "EDIT_COUNTER="
            EditCounter = Val(Mid(Line$, 14))
          ELSEIF Left(Line$, 14) = "PHOTO_COUNTER="
            PhotoCounter = Val(Mid(Line$, 15))
          ENDIF
          Continue
        ENDIF
        
        IF Line$ = "---"
          Continue
        ENDIF
        
        IF Left(Line$, 5) = "TYPE="
          AddElement(Objects())
          Objects()\Type = Mid(Line$, 6)
        ELSEIF Left(Line$, 2) = "X="
          Objects()\X = Val(Mid(Line$, 3))
        ELSEIF Left(Line$, 2) = "Y="
          Objects()\Y = Val(Mid(Line$, 3))
        ELSEIF Left(Line$, 6) = "WIDTH="
          Objects()\Width = Val(Mid(Line$, 7))
        ELSEIF Left(Line$, 7) = "HEIGHT="
          Objects()\Height = Val(Mid(Line$, 8))
        ELSEIF Left(Line$, 5) = "TEXT="
          Objects()\Text = Mid(Line$, 6)
        ELSEIF Left(Line$, 10) = "IMAGEPATH="
          Objects()\ImagePath = Mid(Line$, 11)
        ENDIF
      Wend
      CloseFile(0)
      
      RedrawCanvas()
      MessageRequester("Success", "Design loaded!", #PB_MessageRequester_Info)
    ENDIF
  ENDIF
EndProcedure

Procedure DeleteSelected()
    PROTECTED isDelete.l = 0
    ForEach Objects()
       IF Objects()\isSelected = 1
          DeleteElement(Objects())
          isDelete = 1
       ENDIF       
    Next
    
    IF isDelete > 0
       RedrawCanvas()
    ENDIF
EndProcedure

Procedure AlignLeft()
   PROTECTED MinX.l = 999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         IF Objects()\X < MinX
            MinX = Objects()\X
         ENDIF
      ENDIF
   Next
   
   IF MinX <> 999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\X = MinX
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignRight()
   PROTECTED MaxRight.l = -999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         PROTECTED RightEdge.l = Objects()\X + Objects()\Width
         IF RightEdge > MaxRight
            MaxRight = RightEdge
         ENDIF
      ENDIF
   Next
   
   IF MaxRight <> -999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\X = MaxRight - Objects()\Width
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignTop()
   PROTECTED MinY.l = 999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         IF Objects()\Y < MinY
            MinY = Objects()\Y
         ENDIF
      ENDIF
   Next
   
   IF MinY <> 999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\Y = MinY
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignBottom()
   PROTECTED MaxBottom.l = -999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         PROTECTED BottomEdge.l = Objects()\Y + Objects()\Height
         IF BottomEdge > MaxBottom
            MaxBottom = BottomEdge
         ENDIF
      ENDIF
   Next
   
   IF MaxBottom <> -999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\Y = MaxBottom - Objects()\Height
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure MatchSize()
   IF CheckTotalSelected() < 2
      ProcedureReturn
   ENDIF
   
   PROTECTED FirstIdx.l = GetFirstSelectedIndex()
   IF FirstIdx < 0
      ProcedureReturn
   ENDIF
   
   SelectElement(Objects(), FirstIdx)
   PROTECTED TargetWidth.l = Objects()\Width
   PROTECTED TargetHeight.l = Objects()\Height
   
   ForEach Objects()
      IF Objects()\isSelected = 1 And ListIndex(Objects()) <> FirstIdx
         Objects()\Width = TargetWidth
         Objects()\Height = TargetHeight
      ENDIF
   Next
   
   RedrawCanvas()
EndProcedure

Procedure DistributeVertical()
   SpacingStr$ = GetGadgetText(#String_Spacing)
   PROTECTED CustomSpacing.l = Val(SpacingStr$)
   IF CustomSpacing < 0 : CustomSpacing = 5 : ENDIF
   
   NewList YPositions.FormObject()
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         AddElement(YPositions())
         YPositions()\Y = Objects()\Y
         YPositions()\Height = Objects()\Height
         YPositions()\X = ListIndex(Objects())
      ENDIF
   Next
   
   SortStructuredList(YPositions(), #PB_Sort_Ascending, OffsetOf(FormObject\Y), TypeOf(FormObject\Y))
   
   FirstElement(YPositions())
   PROTECTED CurrentY.l = YPositions()\Y
   
   ForEach YPositions()
      PROTECTED ObjIdx.l = YPositions()\X
      SelectElement(Objects(), ObjIdx)
      Objects()\Y = SnapToGrid(CurrentY)
      CurrentY = Objects()\Y + Objects()\Height + CustomSpacing
   Next
   
   RedrawCanvas()
EndProcedure

Procedure RenumberObjects()
   PROTECTED BtnNum.l = 0
   PROTECTED LblNum.l = 0
   PROTECTED EdtNum.l = 0
   PROTECTED PhtNum.l = 0
   
   ForEach Objects()
      SELECT Objects()\Type
         CASE "BTN"
            BtnNum + 1
            Objects()\Text = "Button " + RSet(Str(BtnNum), 2, "0")
         CASE "LABEL"
            LblNum + 1
            Objects()\Text = "Label " + RSet(Str(LblNum), 2, "0")
         CASE "EDIT"
            EdtNum + 1
            Objects()\Text = "Edit " + RSet(Str(EdtNum), 2, "0")
         CASE "PHOTO"
            PhtNum + 1
            Objects()\Text = "Photo " + RSet(Str(PhtNum), 2, "0")
      ENDSELECT
   Next
   
   ButtonCounter = BtnNum
   LabelCounter = LblNum
   EditCounter = EdtNum
   PhotoCounter = PhtNum
   
   RedrawCanvas()
EndProcedure

Procedure SelectALL(ALL_0_Lable_1_EDIT_2)
   ForEach Objects()
      IF ALL_0_Lable_1_EDIT_2 = 2 And Objects()\Type <> "EDIT"
         Continue
      ELSEIF ALL_0_Lable_1_EDIT_2 = 1 And Objects()\Type <> "LABEL"
         Continue
      ENDIF
      Objects()\isSelected = 1
      SelectionCounter +1
      Objects()\SelectionOrder = SelectionCounter
   Next
   RedrawCanvas()
EndProcedure

Procedure NewDesign()
  Result.i = MessageRequester("New Design", "Clear all objects and start new?", #PB_MessageRequester_YesNo)
  IF Result = #PB_MessageRequester_Yes
    ClearList(Objects())
    ButtonCounter = 0
    LabelCounter = 0
    EditCounter = 0
    PhotoCounter = 0
    RedrawCanvas()
  ENDIF
EndProcedure

OpenWindow(#Win_Main, 0, 0, 900, 650, "My DB Form Designer "+#Version$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered | #PB_Window_SizeGadget | #PB_Window_MaximizeGadget)

CanvasGadget(#Canvas_Designer, 10, 10, 700, 550)

TextGadget(#LABEL_OBJECT, 720, 10, 160, 20, "Object:")
ButtonGadget(#Btn_AddButton, 720, 30, 160, 28, "Add Button")
ButtonGadget(#Btn_AddLabel, 720, 65, 160, 28, "Add Label")
ButtonGadget(#Btn_AddEdit, 720, 100, 160, 28, "Add Edit")
ButtonGadget(#Btn_AddPhoto, 720, 135, 160, 28, "Add Photo")
ButtonGadget(#Btn_Delete, 720, 170, 160, 28, "Delete Selected")

TextGadget(#LABEL_ALIGNMENT, 720, 210, 160, 20, "Alignment:")
ButtonGadget(#Btn_AlignLeft, 720, 235, 75, 25, "Left")
ButtonGadget(#Btn_AlignRight, 805, 235, 75, 25, "Right")
ButtonGadget(#Btn_AlignTop, 720, 265, 75, 25, "Top")
ButtonGadget(#Btn_AlignBottom, 805, 265, 75, 25, "Bottom")
ButtonGadget(#Btn_MatchSize, 720, 295, 160, 25, "Match Size (1st)")
ButtonGadget(#Btn_DistributeV, 720, 325, 160, 25, "Distribute Vertical")
ButtonGadget(#Btn_Renumber, 720, 355, 160, 25, "Renumber Objects")

TextGadget(#Text_Spacing, 720, 390, 100, 20, "V-Spacing (px):")
StringGadget(#String_Spacing, 820, 388, 60, 22, "5")

TextGadget(#LABEL_FILE, 720, 420, 160, 20, "File:")
ButtonGadget(#Btn_New, 720, 440, 160, 28, "New Design")
ButtonGadget(#Btn_Save, 720, 475, 160, 28, "Save Design (Full)")
ButtonGadget(#Btn_SaveClean, 720, 510, 160, 28, "Save Design (Clean)")
ButtonGadget(#Btn_Load, 720, 545, 160, 28, "Load Design")

TextGadget(#Text_Info, 10, 570, 400, 60, "Ctrl+Click: multi-select. Drag: move. Handle: resize. Box-select: drag empty area." + #LF$ + "Selection confirmed on mouse release. Objects snap to 5px grid.")

ButtonGadget(#Btn_SelectALL, 420, 570, 80, 25, "Select ALL")
ButtonGadget(#Btn_SelectLabel, 510, 570, 90, 25, "Select Label")
ButtonGadget(#Btn_SelectEDIT, 610, 570, 80, 25, "Select EDIT")

OffscreenImage = CreateImage(#PB_Any, 700, 550)

DrawGrid()

Procedure ResizeControls()
  PROTECTED WinW.l = WindowWidth(#Win_Main)
  PROTECTED WinH.l = WindowHeight(#Win_Main)
  PROTECTED CanvasW.l = WinW - 190
  PROTECTED CanvasH.l = WinH - 90
  PROTECTED InfoY.l = WinH - 70
  PROTECTED ButtonX.l = WinW - 170
  PROTECTED SelectButtonY.l = WinH - 70
  
  IF CanvasW < 400 : CanvasW = 400 : ENDIF
  IF CanvasH < 300 : CanvasH = 300 : ENDIF
  
  ResizeGadget(#Canvas_Designer, 10, 10, CanvasW, CanvasH)
  ResizeGadget(#LABEL_OBJECT, ButtonX, 10, 160, 20)
  ResizeGadget(#Btn_AddButton, ButtonX, 30, 160, 27)
  ResizeGadget(#Btn_AddLabel, ButtonX, 65, 160, 27)
  ResizeGadget(#Btn_AddEdit, ButtonX, 100, 160, 27)
  ResizeGadget(#Btn_AddPhoto, ButtonX, 135, 160, 27)
  ResizeGadget(#Btn_Delete, ButtonX, 170, 160, 27)
  ResizeGadget(#LABEL_ALIGNMENT, ButtonX, 210, 160, 20)
  ResizeGadget(#Btn_AlignLeft, ButtonX, 235, 75, 25)
  ResizeGadget(#Btn_AlignRight, ButtonX + 85, 235, 75, 25)
  ResizeGadget(#Btn_AlignTop, ButtonX, 265, 75, 25)
  ResizeGadget(#Btn_AlignBottom, ButtonX + 85, 265, 75, 25)
  ResizeGadget(#Btn_MatchSize, ButtonX, 295, 160, 25)
  ResizeGadget(#Btn_DistributeV, ButtonX, 325, 160, 25)
  ResizeGadget(#Btn_Renumber, ButtonX, 355, 160, 25)
  ResizeGadget(#Text_Spacing, ButtonX, 390, 100, 20)
  ResizeGadget(#String_Spacing, ButtonX + 100, 388, 60, 22)
  ResizeGadget(#LABEL_FILE, ButtonX, 420, 160, 20)
  ResizeGadget(#Btn_New, ButtonX, 440, 160, 27)
  ResizeGadget(#Btn_Save, ButtonX, 475, 160, 27)
  ResizeGadget(#Btn_SaveClean, ButtonX, 510, 160, 27)
  ResizeGadget(#Btn_Load, ButtonX, 545, 160, 27)
  ResizeGadget(#Text_Info, 10, InfoY, 400, 60)
  ResizeGadget(#Btn_SelectALL, 420, SelectButtonY, 80, 25)
  ResizeGadget(#Btn_SelectLabel, 510, SelectButtonY, 90, 25)
  ResizeGadget(#Btn_SelectEDIT, 610, SelectButtonY, 80, 25)
  
  IF IsImage(OffscreenImage)
    FreeImage(OffscreenImage)
  ENDIF
  OffscreenImage = CreateImage(#PB_Any, CanvasW, CanvasH)
  
  RedrawCanvas()
EndProcedure

Repeat
  Event = WaitWindowEvent()
  
  SELECT Event
    CASE #PB_Event_SizeWindow
      ResizeControls()
      
    CASE #PB_Event_Gadget
      SELECT EventGadget()
        CASE #Btn_AddButton
          AddObject("BTN")
          RedrawCanvas()
        CASE #Btn_AddLabel
          AddObject("LABEL")
          RedrawCanvas()
        CASE #Btn_AddEdit
          AddObject("EDIT")
          RedrawCanvas()
        CASE #Btn_AddPhoto
          AddObject("PHOTO")
          RedrawCanvas()
        CASE #Btn_Delete : DeleteSelected()
        CASE #Btn_AlignLeft : AlignLeft()
        CASE #Btn_AlignRight : AlignRight()
        CASE #Btn_AlignTop : AlignTop()
        CASE #Btn_AlignBottom : AlignBottom()
        CASE #Btn_MatchSize : MatchSize()
        CASE #Btn_DistributeV : DistributeVertical()
        CASE #Btn_Renumber : RenumberObjects()
        CASE #Btn_New : NewDesign()
        CASE #Btn_Save : SaveFile(1)
        CASE #Btn_SaveClean : SaveFile(0)
        CASE #Btn_Load : LoadDesign()
        CASE #Btn_SelectALL : SelectALL(0)
        CASE #Btn_SelectLabel : SelectALL(1)
        CASE #Btn_SelectEDIT : SelectALL(2)
           
        CASE #Canvas_Designer
          SELECT EventType()
            CASE #PB_EventType_LeftButtonDown
              X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
              Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
              ClickedObj.i = FindObjectAt(X, Y)
              HasMoved = #False
              
              PendingClickObj = ClickedObj
              PendingCtrlPressed = #False
              IF GetAsyncKeyState_(#VK_CONTROL) & $8000
                PendingCtrlPressed = #True
              ENDIF
              
              IF ClickedObj >= 0
                IF CheckTotalSelected() = 1 And IsSelected(ClickedObj)
                  ResizeEdge = CheckResizeHandle(X, Y, ClickedObj)
                ELSE
                  ResizeEdge = ""
                ENDIF
                
                IF ResizeEdge <> ""
                  ResizeMode = #True
                  DragStartX = X
                  DragStartY = Y
                  SaveSnapshots()
                ELSE
                  IF IsSelected(ClickedObj)
                    DragMode = #True
                    DragStartX = X
                    DragStartY = Y
                    SaveSnapshots()
                  ENDIF
                ENDIF
              ELSE
                ; Start selection box
                SelectionBoxActive = #True
                SelectionBoxStartX = X
                SelectionBoxStartY = Y
                SelectionBoxEndX = X
                SelectionBoxEndY = Y
                
                IF Not PendingCtrlPressed
                  ClearSelection()
                ENDIF
              ENDIF
              
            CASE #PB_EventType_LeftButtonUp
              IF SelectionBoxActive
                SelectObjectsInBox()
                SelectionBoxActive = #False
                RedrawCanvas()
              ELSEIF PendingClickObj >= 0 And Not HasMoved And Not ResizeMode
                IF PendingCtrlPressed
                  IF IsSelected(PendingClickObj)
                    RemoveSelection(PendingClickObj)
                  ELSE
                    AddSelection(PendingClickObj)
                  ENDIF
                ELSE
                  ClearSelection()
                  AddSelection(PendingClickObj)
                ENDIF
                RedrawCanvas()
              ELSEIF PendingClickObj < 0 And Not HasMoved And Not PendingCtrlPressed And Not SelectionBoxActive
                ClearSelection()
                RedrawCanvas()
              ENDIF
              
              DragMode = #False
              ResizeMode = #False
              ResizeEdge = ""
              PendingClickObj = -1
              HasMoved = #False
              
            CASE #PB_EventType_MouseMove
              X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
              Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
              
              IF SelectionBoxActive
                HasMoved = #True
                SelectionBoxEndX = X
                SelectionBoxEndY = Y
                RedrawCanvas()
                
              ELSEIF ResizeMode And CheckTotalSelected() >= 1
                HasMoved = #True
                
                DX = X - DragStartX
                DY = Y - DragStartY
                
                Define SnapIdx.l = 0
                ForEach Objects()
                  IF Objects()\isSelected
                    IF SelectElement(SnapShots(), SnapIdx)
                      NewW.l = SnapShots()\Width + DX
                      NewH.l = SnapShots()\Height + DY
                      
                      IF NewW < 20 : NewW = 20 : ENDIF
                      IF NewH < 20 : NewH = 20 : ENDIF
                      
                      Objects()\Width = SnapToGrid(NewW)
                      Objects()\Height = SnapToGrid(NewH)
                      
                      SnapIdx + 1
                    ENDIF
                  ENDIF
                Next
                RedrawCanvas()
                
              ELSEIF DragMode And CheckTotalSelected() > 0
                HasMoved = #True
                
                DX = X - DragStartX
                DY = Y - DragStartY
                
                SnapIdx = 0
                ForEach Objects()
                  IF Objects()\isSelected
                    IF SelectElement(SnapShots(), SnapIdx)
                      Objects()\X = SnapToGrid(SnapShots()\X + DX)
                      Objects()\Y = SnapToGrid(SnapShots()\Y + DY)
                      
                      IF Objects()\X < 0 : Objects()\X = 0 : ENDIF
                      IF Objects()\Y < 0 : Objects()\Y = 0 : ENDIF
                      
                      SnapIdx + 1
                    ENDIF
                  ENDIF
                Next
                RedrawCanvas()
              ELSE
                UpdateMouseCursor(X, Y)
              ENDIF
          ENDSELECT
      ENDSELECT
      
    CASE #PB_Event_CloseWindow
      Break
  ENDSELECT
ForEver

End

Mesa
Enthusiast
Enthusiast
Posts: 461
Joined: Fri Feb 24, 2012 10:19 am

Re: updated: Simple and useful form layout designer for DBase / SQLite and others

Post by Mesa »

line 221: Min is not a function...

M.
infratec
Always Here
Always Here
Posts: 7771
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: updated: Simple and useful form layout designer for DBase / SQLite and others

Post by infratec »

moricode
Enthusiast
Enthusiast
Posts: 167
Joined: Thu May 25, 2023 3:55 am

Re: updated: Simple and useful form layout designer for DBase / SQLite and others

Post by moricode »

Mesa wrote: Sat Jan 10, 2026 9:50 am line 221: Min is not a function...

M.
i had forgotted to put this in

Code: Select all

;  QuickHelp Min(integer1, integer2) 
ProcedureDLL.l Min(integer1, integer2)      ; return integer Min of value
  IF integer1<integer2 :     ProcedureReturn integer1  :  ELSE  :       ProcedureReturn integer2  :  ENDIF
EndProcedure
moricode
Enthusiast
Enthusiast
Posts: 167
Joined: Thu May 25, 2023 3:55 am

Re: updated: version 1.2

Post by moricode »

Update :New version 1.2 pro

; version v1.2:
; simplify the object struct
; remove ObjectSnapshot() , with better dragging design
; Improve the redrawObject() when in drag/Resize move , draw the dynamic dragging and sizing preview
; limit move object in left bound and top bound
; improve robustness of LoadDesign() logic
; Added minimum object size 20x20 limit check
; Remove unuse variable
Todo :
; Store chace Total_select when done select , this calculate once not everytime call CheckTotalSelected() function


Code: Select all



; Visual Form Designer in PureBasic 5.7x , 6.1x , 6.2x
; Features: BTN, LABEL, EDIT, PHOTO objects with 5px grid snapping
; Enhanced: Dynamic resize, Move cursor, Selection box

; v1.2 simplfy the object struct, no need ObjectSnapshot() 
; Improve the redrawObject() when in drag/Resize move , draw the preview new position and size 
; limit move object in left bound and top bound 
; Remove unuse variable
; improve robustness of LoadDesign() logic
; Store chace Total_select when done select , this calculate once not everytime call CheckTotalSelected() function
; Added minimum object size 20x20 limit check


#Version$ = "1.2 pro"
Structure FormObject
   Type.s
   X.l
   Y.l
   W.l
   H.l
   Text.s
;   ImagePath.s
   isSelected.l
   SelectionOrder.l
EndStructure

#GRID_SIZE = 5
#MAX_OBJECTS = 100

Global NewList Objects.FormObject()
Global DragMode.i , ResizeMode.i 
Global ResizeEdge.s = ""
Global DragStartX.l, DragStartY.l
Global OffscreenImage.i
Global SelectionCounter.l = 0
Global PendingClickObj.i = -1
Global PendingCtrlPressed.i = #False
Global HasMoved.i = #False
Global ButtonCounter , LabelCounter , EditCounter , PhotoCounter 

; Selection box variables
Global SelectionBoxActive.i = #False
Global SelectionBoxStartX, SelectionBoxStartY , SelectionBoxEndX, SelectionBoxEndY
Global DX_Offset , DY_Offset

#RESIZE_HANDLE = 8

Enumeration
   #Win_Main
   #Canvas_Designer  
   #Text_Info
   #Btn_AddButton
   #Btn_AddLabel
   #Btn_AddEdit
   #Btn_AddPhoto
   #Btn_Delete
   #Btn_Save
   #Btn_SaveClean
   #Btn_Load
   #Btn_New
   #Btn_AlignLeft
   #Btn_AlignRight
   #Btn_AlignTop
   #Btn_AlignBottom
   #Btn_DistributeV
   #Btn_MatchSize
   #Btn_Renumber
   #Text_Spacing
   #String_Spacing
   #LABEL_OBJECT
   #LABEL_ALIGNMENT
   #LABEL_FILE  
   #Btn_SelectALL
   #Btn_SelectLabel
   #Btn_SelectEDIT
EndEnumeration

Procedure Min(integer1, integer2)     
  IF integer1<integer2 :     ProcedureReturn integer1  :  ELSE  :       ProcedureReturn integer2  :  ENDIF
EndProcedure

Procedure SnapToGrid(Value.l)
   ProcedureReturn Round(Value / #GRID_SIZE, #PB_Round_Nearest) * #GRID_SIZE
EndProcedure

Procedure IsSelected(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   IF Objects()\isSelected = 1
      ProcedureReturn #True
   ENDIF
   ProcedureReturn #False
EndProcedure

Procedure CheckTotalSelected()
   PROTECTED Total.l = 0
   ForEach Objects()
      IF Objects()\isSelected
         Total = Total + 1
      ENDIF
   Next
   ProcedureReturn Total
EndProcedure

Procedure CheckSelected()
   ForEach Objects()
      IF Objects()\isSelected
         ProcedureReturn ListIndex(Objects())
      ENDIF                      
   Next
   ProcedureReturn -1
EndProcedure

Procedure GetFirstSelectedIndex()
   PROTECTED MinOrder.l = 999999
   PROTECTED FirstIdx.l = -1
   PROTECTED idx.l = 0
   
   ForEach Objects()
      IF Objects()\isSelected
         IF Objects()\SelectionOrder < MinOrder
            MinOrder = Objects()\SelectionOrder
            FirstIdx = idx
         ENDIF
      ENDIF
      idx + 1
   Next
   ProcedureReturn FirstIdx
EndProcedure

Procedure AddSelection(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   Objects()\isSelected = 1
   SelectionCounter + 1
   Objects()\SelectionOrder = SelectionCounter
EndProcedure

Procedure RemoveSelection(ObjIdx.i)
   SelectElement(Objects(),ObjIdx)
   Objects()\isSelected = 0
   Objects()\SelectionOrder = 0
EndProcedure

Procedure ClearSelection()
   ForEach Objects()
      Objects()\isSelected = 0
      Objects()\SelectionOrder = 0
   Next
   SelectionCounter = 0
EndProcedure

Procedure DrawGrid()
   IF StartDrawing(ImageOutput(OffscreenImage))
      DrawingMode(#PB_2DDrawing_Default)
      Box(0, 0, OutputWidth(), OutputHeight(), RGB(255, 255, 255))
      For x.l = 0 To OutputWidth() Step #GRID_SIZE
         For y.l = 0 To OutputHeight() Step #GRID_SIZE
            LineXY(x, y, x, y, RGB(220, 220, 220))
         Next
      Next
      StopDrawing()
   ENDIF
EndProcedure

Procedure DrawObjects()
   PROTECTED X,Y,W,H
   IF StartDrawing(ImageOutput(OffscreenImage))
      ForEach Objects()
         X = Objects()\X  
         Y = Objects()\Y  
         W = Objects()\W
         H = Objects()\H
         
         ; Apply offset ONLY for preview during drag/resize
         IF Objects()\isSelected = 1
            IF DragMode 
               X + DX_Offset
               Y + DY_Offset
            ELSEIF ResizeMode
               W + DX_Offset
               H + DY_Offset
               ; Enforce minimum size during preview
               IF W < 20 : W = 20 : ENDIF
               IF H < 20 : H = 20 : ENDIF
            ENDIF
         ENDIF
         
         SELECT Objects()\Type
            CASE "BTN"
               DrawingMode(#PB_2DDrawing_Default)
               Box(X, Y, W, H, RGB(240, 240, 240))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(X, Y, W, H, RGB(0, 0, 0))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(X + 3, Y + 3, Objects()\Text, RGB(0, 0, 0))
            CASE "LABEL"
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(X+3, Y+3, Objects()\Text, RGB(0, 0, 0))               
            CASE "EDIT"
               DrawingMode(#PB_2DDrawing_Default)
               Box(X, Y, W, H, RGB(255, 255, 255))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(X, Y, W, H, RGB(128, 128, 128))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(X + 3, Y + 3, Objects()\Text, RGB(128, 128, 128))               
            CASE "PHOTO"
               DrawingMode(#PB_2DDrawing_Default)
               Box(X, Y, W, H, RGB(200, 200, 200))
               DrawingMode(#PB_2DDrawing_Outlined)
               Box(X, Y, W, H, RGB(0, 0, 0))
               DrawingMode(#PB_2DDrawing_Transparent)
               DrawText(X + W/2 - 25, Y + H/2 - 5, Objects()\Text, RGB(100, 100, 100))               
         ENDSELECT
         
         IF Objects()\isSelected  
            DrawingMode(#PB_2DDrawing_Outlined)
            Box(X - 2, Y - 2, W + 4, H + 4, RGB(0, 120, 215))
            DrawingMode(#PB_2DDrawing_Default)
            Box(X + W - #RESIZE_HANDLE, Y + H - #RESIZE_HANDLE, #RESIZE_HANDLE, #RESIZE_HANDLE, RGB(0, 120, 215))
            Box(X + W - #RESIZE_HANDLE + 1, Y + H - #RESIZE_HANDLE + 1, #RESIZE_HANDLE - 2, #RESIZE_HANDLE - 2, RGB(255, 255, 255))   
         ENDIF    
      Next
      
      ; Draw selection box
      IF SelectionBoxActive
         PROTECTED BoxX = Min(SelectionBoxStartX, SelectionBoxEndX)
         PROTECTED BoxY = Min(SelectionBoxStartY, SelectionBoxEndY)
         PROTECTED BoxW = Abs(SelectionBoxEndX - SelectionBoxStartX)
         PROTECTED BoxH = Abs(SelectionBoxEndY - SelectionBoxStartY)
         DrawingMode(#PB_2DDrawing_Outlined)
         Box(BoxX, BoxY, BoxW, BoxH, RGB(0, 120, 215))
      ENDIF
      
      StopDrawing()
   ENDIF
EndProcedure

Procedure FindEmptySpace(*Width.Long, *Height.Long)
   ; input the W,H , but output the found X,Y
   PROTECTED TestX, TestY  , Found
   PROTECTED CanvasH = GadgetHeight(#Canvas_Designer)
   PROTECTED CanvasW = GadgetWidth(#Canvas_Designer)
   PROTECTED ObjWidth = *Width\l
   PROTECTED ObjHeight = *Height\l
   
   ; object always search from 5,5, and then check overlaps every 5 pixel vertical 
   ; and every 20 pixel horizontal for the best position
   
   For TestX = 5 To CanvasW - ObjWidth Step 20
      For TestY = 5 To CanvasH - ObjHeight Step 5
         Found = #True
         
         ; Check for overlap with existing objects
         IF ListSize(Objects()) > 0
            ForEach Objects()
               IF Not (TestX + ObjWidth <= Objects()\X Or TestX >= Objects()\X + Objects()\W Or
                       TestY + ObjHeight <= Objects()\Y Or TestY >= Objects()\Y + Objects()\H)
                  ; Overlap detected
                  Found = #False
                  Break
               ENDIF
            Next
         ENDIF
         
         IF Found
            *Width\l = TestX
            *Height\l = TestY
            ProcedureReturn #True
         ENDIF
      Next
   Next
   
   ; If no space found, return default position
   *Width\l = 5
   *Height\l = 5
   ProcedureReturn #False
EndProcedure

Procedure AddObject(Type.s)
   PROTECTED NewX, NewY , TempW, TempH
   
   SELECT Type
      CASE "BTN"
         TempW = 100
         TempH = 22
         ButtonCounter + 1
         T$ = "Button " + RSet(Str(ButtonCounter), 2, "0")
      CASE "LABEL"
         TempW = 60
         TempH = 20
         LabelCounter + 1
         T$ = "Label " + RSet(Str(LabelCounter), 2, "0")
      CASE "EDIT"
         TempW = 150
         TempH = 20
         EditCounter + 1
         T$ = "Edit " + RSet(Str(EditCounter), 2, "0")
      CASE "PHOTO"
         TempW = 100
         TempH = 100
         PhotoCounter + 1
         T$ = "Photo " + RSet(Str(PhotoCounter), 2, "0")
   ENDSELECT
   
   NewX = TempW
   NewY = TempH
   FindEmptySpace(@NewX, @NewY)
   AddElement(Objects())
   Objects()\Type = Type
   Objects()\X = NewX
   Objects()\Y = NewY
   Objects()\Text = T$
   Objects()\W = TempW
   Objects()\H = TempH
   
   ClearSelection()
   AddSelection(ListSize(Objects()) - 1)
EndProcedure

Procedure FindObjectAt(X.l, Y.l)
   PROTECTED idx
   
   IF ListSize(Objects()) = 0
      ProcedureReturn -1
   ENDIF
   
   idx = ListSize(Objects()) - 1
   LastElement(Objects())
   
   Repeat
      IF X >= Objects()\X And X <= Objects()\X + Objects()\W And 
         Y >= Objects()\Y And Y <= Objects()\Y + Objects()\H
         ProcedureReturn idx
      ENDIF
      idx - 1
   Until Not PreviousElement(Objects())
   
   ProcedureReturn -1
EndProcedure

Procedure.s CheckResizeHandle(X.l, Y.l, ObjIdx.i)
   SelectElement(Objects(),ObjIdx)   
   IF Objects()\isSelected = 1
      IF X >= Objects()\X + Objects()\W - #RESIZE_HANDLE And 
         X <= Objects()\X + Objects()\W And
         Y >= Objects()\Y + Objects()\H - #RESIZE_HANDLE And
         Y <= Objects()\Y + Objects()\H
         ProcedureReturn "SE"
      ENDIF
   ENDIF
   ProcedureReturn ""
EndProcedure

Procedure UpdateMouseCursor(X.l, Y.l)
   PROTECTED Obj.i = FindObjectAt(X, Y)
   
   IF Obj >= 0 And IsSelected(Obj)
      IF CheckTotalSelected() = 1 ;And IsSelected(Obj)
         PROTECTED Edge.s = CheckResizeHandle(X, Y, Obj)
         IF Edge = "SE"
            SetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Cursor, #PB_Cursor_LeftUpRightDown)
            ProcedureReturn
         ENDIF
      ENDIF
      
      SetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Cursor,#PB_Cursor_Arrows); #PB_Cursor_Cross)  ; multi 
      ProcedureReturn
   ENDIF
   
   SetGadgetAttribute(#Canvas_Designer, #PB_Canvas_Cursor, #PB_Cursor_Default)
EndProcedure

Procedure SelectObjectsInBox()
   PROTECTED BoxX.l = Min(SelectionBoxStartX, SelectionBoxEndX)
   PROTECTED BoxY.l = Min(SelectionBoxStartY, SelectionBoxEndY)
   PROTECTED BoxW.l = Abs(SelectionBoxEndX - SelectionBoxStartX)
   PROTECTED BoxH.l = Abs(SelectionBoxEndY - SelectionBoxStartY)
   PROTECTED BoxRight.l = BoxX + BoxW
   PROTECTED BoxBottom.l = BoxY + BoxH
   
   ForEach Objects()
      PROTECTED ObjRight.l = Objects()\X + Objects()\W
      PROTECTED ObjBottom.l = Objects()\Y + Objects()\H
      
      IF Not (Objects()\X > BoxRight Or ObjRight < BoxX Or 
              Objects()\Y > BoxBottom Or ObjBottom < BoxY)
         IF Not Objects()\isSelected
            AddSelection(ListIndex(Objects()))
         ENDIF
      ENDIF
   Next
EndProcedure

Procedure RedrawCanvas()
   DrawGrid()
   DrawObjects()
   IF StartDrawing(CanvasOutput(#Canvas_Designer))
      DrawImage(ImageID(OffscreenImage), 0, 0)
      StopDrawing()
   ENDIF
EndProcedure

Procedure SaveFile(bFull)
   File$ = SaveFileRequester("Save Design", "design.txt", "Text Files (*.txt)|*.txt", 0)
   IF File$
      IF CreateFile(0, File$)
         IF bFull
            WriteStringN(0, "BUTTON_COUNTER=" + Str(ButtonCounter))
            WriteStringN(0, "LABEL_COUNTER=" + Str(LabelCounter))
            WriteStringN(0, "EDIT_COUNTER=" + Str(EditCounter))
            WriteStringN(0, "PHOTO_COUNTER=" + Str(PhotoCounter))
            WriteStringN(0, "")
         ENDIF
         ForEach Objects()
            WriteStringN(0, "TYPE=" + Objects()\Type)
            WriteStringN(0, "X=" + Str(Objects()\X))
            WriteStringN(0, "Y=" + Str(Objects()\Y))
            WriteStringN(0, "W=" + Str(Objects()\W))
            WriteStringN(0, "H=" + Str(Objects()\H))
            IF bFull = 1
               WriteStringN(0, "TEXT=" + Objects()\Text)
               WriteStringN(0, "---")
            ENDIF
         Next
         CloseFile(0)
         MessageRequester("Success", "Design saved!", #PB_MessageRequester_Info)
      ENDIF
   ENDIF
EndProcedure

Procedure LoadDesign()
   File$ = OpenFileRequester("Load Design", "", "Text Files (*.txt)|*.txt", 0)
   IF File$ And FileSize(File$) > 0
      ClearList(Objects())
      ClearSelection()
      
      ButtonCounter = 0
      LabelCounter = 0
      EditCounter = 0
      PhotoCounter = 0
      
      IF ReadFile(0, File$)
         PROTECTED InCounterSection.i = #False
         
         While Not Eof(0)
            Line$ = ReadString(0)
            CMD$ = Trim(StringField(Line$,1,"="))
            Value$ = StringField(Line$,2,"=")
            Value = Val(Value$)
            
            SELECT CMD$ 
               CASE "BUTTON_COUNTER"
                  ButtonCounter = Value
               CASE "LABEL_COUNTER"
                  LabelCounter = Value
               CASE "EDIT_COUNTER"
                  EditCounter = Value
               CASE "PHOTO_COUNTER"
                  PhotoCounter = Value              
               CASE "TYPE"
                  SELECT Value$
                     CASE "BTN","LABEL","EDIT","PHOTO"                  
                        AddElement(Objects())
                        Objects()\Type = Value$         
                  ENDSELECT
            ENDSELECT
            
            IF ListSize(Objects())
               SELECT CMD$
                  CASE "X"
                     Objects()\X = Value
                  CASE "Y"
                     Objects()\Y = Value
                  CASE "W"
                     Objects()\W = Value
                  CASE "H"
                     Objects()\H = Value
                  CASE "TEXT"
                     Objects()\Text = Value$
               ENDSELECT
            ENDIF
            
         Wend
         CloseFile(0)
         
         RedrawCanvas()
         MessageRequester("Success", "Design loaded!", #PB_MessageRequester_Info)
      ENDIF
   ENDIF
EndProcedure

Procedure DeleteSelected()
   PROTECTED isDelete.l = 0
   ForEach Objects()
      IF Objects()\isSelected = 1
         DeleteElement(Objects())
         isDelete = 1
      ENDIF       
   Next
   
   IF isDelete > 0
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignLeft()
   PROTECTED MinX.l = 999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         IF Objects()\X < MinX
            MinX = Objects()\X
         ENDIF
      ENDIF
   Next
   
   IF MinX <> 999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\X = MinX
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignRight()
   PROTECTED MaxRight.l = -999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         PROTECTED RightEdge.l = Objects()\X + Objects()\W
         IF RightEdge > MaxRight
            MaxRight = RightEdge
         ENDIF
      ENDIF
   Next
   
   IF MaxRight <> -999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\X = MaxRight - Objects()\W
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignTop()
   PROTECTED MinY.l = 999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         IF Objects()\Y < MinY
            MinY = Objects()\Y
         ENDIF
      ENDIF
   Next
   
   IF MinY <> 999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\Y = MinY
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure AlignBottom()
   PROTECTED MaxBottom.l = -999999
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         PROTECTED BottomEdge.l = Objects()\Y + Objects()\H
         IF BottomEdge > MaxBottom
            MaxBottom = BottomEdge
         ENDIF
      ENDIF
   Next
   
   IF MaxBottom <> -999999
      ForEach Objects()
         IF Objects()\isSelected = 1
            Objects()\Y = MaxBottom - Objects()\H
         ENDIF
      Next
      RedrawCanvas()
   ENDIF
EndProcedure

Procedure MatchSize()
   IF CheckTotalSelected() < 2
      ProcedureReturn
   ENDIF
   
   PROTECTED FirstIdx.l = GetFirstSelectedIndex()
   IF FirstIdx < 0
      ProcedureReturn
   ENDIF
   
   SelectElement(Objects(), FirstIdx)
   PROTECTED TargetWidth.l = Objects()\W
   PROTECTED TargetHeight.l = Objects()\H
   
   ForEach Objects()
      IF Objects()\isSelected = 1 And ListIndex(Objects()) <> FirstIdx
         Objects()\W = TargetWidth
         Objects()\H = TargetHeight
      ENDIF
   Next
   
   RedrawCanvas()
EndProcedure

Procedure DistributeVertical()
   SpacingStr$ = GetGadgetText(#String_Spacing)
   PROTECTED CustomSpacing.l = Val(SpacingStr$)
   IF CustomSpacing < 0 : CustomSpacing = 5 : ENDIF
   
   NewList YPositions.FormObject()
   
   ForEach Objects()
      IF Objects()\isSelected = 1
         AddElement(YPositions())
         YPositions()\Y = Objects()\Y
         YPositions()\H = Objects()\H
         YPositions()\X = ListIndex(Objects())
      ENDIF
   Next
   
   SortStructuredList(YPositions(), #PB_Sort_Ascending, OffsetOf(FormObject\Y), TypeOf(FormObject\Y))
   
   FirstElement(YPositions())
   PROTECTED CurrentY.l = YPositions()\Y
   
   ForEach YPositions()
      PROTECTED ObjIdx.l = YPositions()\X
      SelectElement(Objects(), ObjIdx)
      Objects()\Y = SnapToGrid(CurrentY)
      CurrentY = Objects()\Y + Objects()\H + CustomSpacing
   Next
   
   RedrawCanvas()
EndProcedure

Procedure RenumberObjects()
   PROTECTED BtnNum.l , LblNum.l ,   EdtNum.l ,  PhtNum.l 
   
   ForEach Objects()
      SELECT Objects()\Type
         CASE "BTN"
            BtnNum + 1
            Objects()\Text = "Button " + RSet(Str(BtnNum), 2, "0")
         CASE "LABEL"
            LblNum + 1
            Objects()\Text = "Label " + RSet(Str(LblNum), 2, "0")
         CASE "EDIT"
            EdtNum + 1
            Objects()\Text = "Edit " + RSet(Str(EdtNum), 2, "0")
         CASE "PHOTO"
            PhtNum + 1
            Objects()\Text = "Photo " + RSet(Str(PhtNum), 2, "0")
      ENDSELECT
   Next
   
   ButtonCounter = BtnNum
   LabelCounter = LblNum
   EditCounter  = EdtNum
   PhotoCounter = PhtNum
   
   RedrawCanvas()
EndProcedure

Procedure SelectALL(ALL_0_Lable_1_EDIT_2)
   ForEach Objects()
      IF ALL_0_Lable_1_EDIT_2 = 2 And Objects()\Type <> "EDIT"
         Continue
      ELSEIF ALL_0_Lable_1_EDIT_2 = 1 And Objects()\Type <> "LABEL"
         Continue
      ENDIF
      Objects()\isSelected = 1
      SelectionCounter +1
      Objects()\SelectionOrder = SelectionCounter
   Next
   RedrawCanvas()
EndProcedure

Procedure NewDesign()
   Result.i = MessageRequester("New Design", "Clear all objects and start new?", #PB_MessageRequester_YesNo)
   IF Result = #PB_MessageRequester_Yes
      ClearList(Objects())
      ButtonCounter = 0
      LabelCounter = 0
      EditCounter = 0
      PhotoCounter = 0
      RedrawCanvas()
   ENDIF
EndProcedure

OpenWindow(#Win_Main, 0, 0, 900, 650, "My DB Form Designer "+#Version$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered | #PB_Window_SizeGadget | #PB_Window_MaximizeGadget)

CanvasGadget(#Canvas_Designer, 10, 10, 700, 550)

TextGadget(#LABEL_OBJECT, 720, 10, 160, 20, "Object:")
ButtonGadget(#Btn_AddButton, 720, 30, 160, 28, "Add Button")
ButtonGadget(#Btn_AddLabel, 720, 65, 160, 28, "Add Label")
ButtonGadget(#Btn_AddEdit, 720, 100, 160, 28, "Add Edit")
ButtonGadget(#Btn_AddPhoto, 720, 135, 160, 28, "Add Photo")
ButtonGadget(#Btn_Delete, 720, 170, 160, 28, "Delete Selected")

TextGadget(#LABEL_ALIGNMENT, 720, 210, 160, 20, "Alignment:")
ButtonGadget(#Btn_AlignLeft, 720, 235, 75, 25, "Left")
ButtonGadget(#Btn_AlignRight, 805, 235, 75, 25, "Right")
ButtonGadget(#Btn_AlignTop, 720, 265, 75, 25, "Top")
ButtonGadget(#Btn_AlignBottom, 805, 265, 75, 25, "Bottom")
ButtonGadget(#Btn_MatchSize, 720, 295, 160, 25, "Match Size (1st)")
ButtonGadget(#Btn_DistributeV, 720, 325, 160, 25, "Distribute Vertical")
ButtonGadget(#Btn_Renumber, 720, 355, 160, 25, "Renumber Objects")

TextGadget(#Text_Spacing, 720, 390, 100, 20, "V-Spacing (px):")
StringGadget(#String_Spacing, 820, 388, 60, 22, "5")

TextGadget(#LABEL_FILE, 720, 420, 160, 20, "File:")
ButtonGadget(#Btn_New, 720, 440, 160, 28, "New Design")
ButtonGadget(#Btn_Save, 720, 475, 160, 28, "Save Design (Full)")
ButtonGadget(#Btn_SaveClean, 720, 510, 160, 28, "Save Design (Clean)")
ButtonGadget(#Btn_Load, 720, 545, 160, 28, "Load Design")

TextGadget(#Text_Info, 10, 570, 400, 60, "Ctrl+Click: multi-select. Drag: move. Handle: resize. Box-select: drag empty area." + #LF$ + "Selection confirmed on mouse release. Objects snap to 5px grid.")

ButtonGadget(#Btn_SelectALL, 420, 570, 80, 25, "Select ALL")
ButtonGadget(#Btn_SelectLabel, 510, 570, 90, 25, "Select Label")
ButtonGadget(#Btn_SelectEDIT, 610, 570, 80, 25, "Select EDIT")

OffscreenImage = CreateImage(#PB_Any, 700, 550)

DrawGrid()

Procedure ResizeControls()
   PROTECTED WinW.l = WindowWidth(#Win_Main)
   PROTECTED WinH.l = WindowHeight(#Win_Main)
   PROTECTED CanvasW.l = WinW - 190
   PROTECTED CanvasH.l = WinH - 90
   PROTECTED InfoY.l = WinH - 70
   PROTECTED ButtonX.l = WinW - 170
   PROTECTED SelectButtonY.l = WinH - 70
   
   IF CanvasW < 400 : CanvasW = 400 : ENDIF
   IF CanvasH < 300 : CanvasH = 300 : ENDIF
   
   ResizeGadget(#Canvas_Designer, 10, 10, CanvasW, CanvasH)
   ResizeGadget(#LABEL_OBJECT, ButtonX, 10, 160, 20)
   ResizeGadget(#Btn_AddButton, ButtonX, 30, 160, 27)
   ResizeGadget(#Btn_AddLabel, ButtonX, 65, 160, 27)
   ResizeGadget(#Btn_AddEdit, ButtonX, 100, 160, 27)
   ResizeGadget(#Btn_AddPhoto, ButtonX, 135, 160, 27)
   ResizeGadget(#Btn_Delete, ButtonX, 170, 160, 27)
   ResizeGadget(#LABEL_ALIGNMENT, ButtonX, 210, 160, 20)
   ResizeGadget(#Btn_AlignLeft, ButtonX, 235, 75, 25)
   ResizeGadget(#Btn_AlignRight, ButtonX + 85, 235, 75, 25)
   ResizeGadget(#Btn_AlignTop, ButtonX, 265, 75, 25)
   ResizeGadget(#Btn_AlignBottom, ButtonX + 85, 265, 75, 25)
   ResizeGadget(#Btn_MatchSize, ButtonX, 295, 160, 25)
   ResizeGadget(#Btn_DistributeV, ButtonX, 325, 160, 25)
   ResizeGadget(#Btn_Renumber, ButtonX, 355, 160, 25)
   ResizeGadget(#Text_Spacing, ButtonX, 390, 100, 20)
   ResizeGadget(#String_Spacing, ButtonX + 100, 388, 60, 22)
   ResizeGadget(#LABEL_FILE, ButtonX, 420, 160, 20)
   ResizeGadget(#Btn_New, ButtonX, 440, 160, 27)
   ResizeGadget(#Btn_Save, ButtonX, 475, 160, 27)
   ResizeGadget(#Btn_SaveClean, ButtonX, 510, 160, 27)
   ResizeGadget(#Btn_Load, ButtonX, 545, 160, 27)
   ResizeGadget(#Text_Info, 10, InfoY, 400, 60)
   ResizeGadget(#Btn_SelectALL, 420, SelectButtonY, 80, 25)
   ResizeGadget(#Btn_SelectLabel, 510, SelectButtonY, 90, 25)
   ResizeGadget(#Btn_SelectEDIT, 610, SelectButtonY, 80, 25)
   
   IF IsImage(OffscreenImage)
      FreeImage(OffscreenImage)
   ENDIF
   OffscreenImage = CreateImage(#PB_Any, CanvasW, CanvasH)
   RedrawCanvas()
EndProcedure

Repeat
   Event = WaitWindowEvent()
   
   SELECT Event
      CASE #PB_Event_SizeWindow
         ResizeControls()
         
      CASE #PB_Event_Gadget
         SELECT EventGadget()
            CASE #Btn_AddButton
               AddObject("BTN")
               RedrawCanvas()
            CASE #Btn_AddLabel
               AddObject("LABEL")
               RedrawCanvas()
            CASE #Btn_AddEdit
               AddObject("EDIT")
               RedrawCanvas()
            CASE #Btn_AddPhoto
               AddObject("PHOTO")
               RedrawCanvas()
            CASE #Btn_Delete : DeleteSelected()
            CASE #Btn_AlignLeft : AlignLeft()
            CASE #Btn_AlignRight : AlignRight()
            CASE #Btn_AlignTop : AlignTop()
            CASE #Btn_AlignBottom : AlignBottom()
            CASE #Btn_MatchSize : MatchSize()
            CASE #Btn_DistributeV : DistributeVertical()
            CASE #Btn_Renumber : RenumberObjects()
            CASE #Btn_New : NewDesign()
            CASE #Btn_Save : SaveFile(1)
            CASE #Btn_SaveClean : SaveFile(0)
            CASE #Btn_Load : LoadDesign()
            CASE #Btn_SelectALL : SelectALL(0)
            CASE #Btn_SelectLabel : SelectALL(1)
            CASE #Btn_SelectEDIT : SelectALL(2)
               
            CASE #Canvas_Designer
               SELECT EventType()
                  CASE #PB_EventType_LeftButtonDown
                     X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
                     Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
                     DragStartX = X
                     DragStartY = Y              
                     ClickedObj.i = FindObjectAt(X, Y)
                     HasMoved = #False
                     
                     PendingClickObj = ClickedObj
                     PendingCtrlPressed = #False
                     IF GetAsyncKeyState_(#VK_CONTROL) & $8000
                        PendingCtrlPressed = #True
                     ENDIF
                     
                     IF ClickedObj >= 0
                        IF CheckTotalSelected() = 1 And IsSelected(ClickedObj)
                           ResizeEdge = CheckResizeHandle(X, Y, ClickedObj)
                        ELSE
                           ResizeEdge = ""
                        ENDIF
                        
                        IF ResizeEdge <> ""
                           ResizeMode = #True
                        ELSE
                           IF IsSelected(ClickedObj)
                              DragMode = #True
                           ENDIF
                        ENDIF
                     ELSE
                        ; Start selection box
                        SelectionBoxActive = #True
                        SelectionBoxStartX = X
                        SelectionBoxStartY = Y
                        SelectionBoxEndX = X
                        SelectionBoxEndY = Y
                        
                        IF Not PendingCtrlPressed
                           ClearSelection()
                        ENDIF
                     ENDIF
                     ; ========================================
                  CASE #PB_EventType_LeftButtonUp
                     IF SelectionBoxActive
                        SelectObjectsInBox()
                        SelectionBoxActive = #False
                        RedrawCanvas()
                        
                     ELSEIF PendingClickObj >= 0 And Not HasMoved And Not ResizeMode
                        IF PendingCtrlPressed
                           IF IsSelected(PendingClickObj)
                              RemoveSelection(PendingClickObj)
                           ELSE
                              AddSelection(PendingClickObj)
                           ENDIF
                        ELSE
                           ClearSelection()
                           AddSelection(PendingClickObj)
                        ENDIF
                        RedrawCanvas()
                        
                     ELSEIF PendingClickObj < 0 And Not HasMoved And Not PendingCtrlPressed And Not SelectionBoxActive
                        ClearSelection()
                        RedrawCanvas()
                     ENDIF
                     
                     ; Confirm new position/size - apply offset to Objects()
                     IF (DragMode Or ResizeMode) And HasMoved
                        CanvasW.l = GadgetWidth(#Canvas_Designer)
                        CanvasH.l = GadgetHeight(#Canvas_Designer)
                        
                        ForEach Objects()
                           IF Objects()\isSelected = 1
                              IF DragMode
                                 ; Apply drag offset with bounds checking
                                 NewX.l = Objects()\X + DX_Offset
                                 NewY.l = Objects()\Y + DY_Offset
                                 
                                 ; Clamp to canvas bounds
                                 IF NewX < 0 : NewX = 0 : ENDIF   ; this clam to left and top
                                 IF NewY < 0 : NewY = 0 : ENDIF
                                 ;     IF NewX + Objects()\W > CanvasW : NewX = CanvasW - Objects()\W : ENDIF       ; this is clamp to right and bottom edge
                                 ;     IF NewY + Objects()\H > CanvasH : NewY = CanvasH - Objects()\H : ENDIF
                                 
                                 Objects()\X = SnapToGrid(NewX)
                                 Objects()\Y = SnapToGrid(NewY)
                                 
                              ELSEIF ResizeMode
                                 ; Apply resize offset with minimum size
                                 NewW.l = Objects()\W + DX_Offset
                                 NewH.l = Objects()\H + DY_Offset
                                 
                                 ; Enforce minimum size
                                 IF NewW < 20 : NewW = 20 : ENDIF
                                 IF NewH < 20 : NewH = 20 : ENDIF
                                 
                                 Objects()\W = SnapToGrid(NewW)
                                 Objects()\H = SnapToGrid(NewH)
                                 
                                 IF Objects()\W < 20 : Objects()\W = 20 : ENDIF
                                 IF Objects()\H < 20 : Objects()\H = 20 : ENDIF
                              ENDIF
                           ENDIF
                        Next
                        
                     ENDIF

                     DX_Offset = 0
                     DY_Offset = 0
                     DragMode = #False
                     ResizeMode = #False
                     ResizeEdge = ""
                     PendingClickObj = -1
                     HasMoved = #False
                     RedrawCanvas()  ; needed                     
                     
                     ; ==========================================
                  CASE #PB_EventType_MouseMove
                     X = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseX)
                     Y = GetGadgetAttribute(#Canvas_Designer, #PB_Canvas_MouseY)
                     
                     RawDX.l = X - DragStartX
                     RawDY.l = Y - DragStartY
                     
                     IF SelectionBoxActive
                        HasMoved = #True
                        SelectionBoxEndX = X
                        SelectionBoxEndY = Y
                        RedrawCanvas()
                        
                     ELSEIF ResizeMode And CheckTotalSelected() >= 1
                        HasMoved = #True
                        ; For resize, use raw offset (no edge limiting needed)
                        DX_Offset = RawDX
                        DY_Offset = RawDY
                        RedrawCanvas()
                        
                     ELSEIF DragMode And CheckTotalSelected() > 0
                        HasMoved = #True
                        
                        ; Find the leftmost and topmost selected object
                        MinX.l = 999999
                        MinY.l = 999999
                        
                        ForEach Objects()
                           IF Objects()\isSelected = 1
                              IF Objects()\X < MinX : MinX = Objects()\X : ENDIF
                              IF Objects()\Y < MinY : MinY = Objects()\Y : ENDIF
                           ENDIF
                        Next
                        
                        ; Limit offset so leftmost object doesn't go past left edge (X=0)
                        IF MinX + RawDX < 0
                           RawDX = -MinX
                        ENDIF
                        
                        ; Limit offset so topmost object doesn't go past top edge (Y=0)
                        IF MinY + RawDY < 0
                           RawDY = -MinY
                        ENDIF
                        
                        DX_Offset = RawDX
                        DY_Offset = RawDY
                        
                        RedrawCanvas()
                     ELSE
                        UpdateMouseCursor(X, Y)
                     ENDIF             
                     
               ENDSELECT
         ENDSELECT
         
      CASE #PB_Event_CloseWindow
         Break
   ENDSELECT
ForEver

End


Axolotl
Addict
Addict
Posts: 913
Joined: Wed Dec 31, 2008 3:36 pm

Re: updated: Simple and useful form layout designer for DBase / SQLite and others

Post by Axolotl »

Thanks again for sharing.

IDEA: I'm not sure if it's a rule, but I think it's good practice to include a brief history in the first post when updating.
Advantage: you/we can see the current status at a glance.
See as an example mk-soft's posts like Window ListIconGadget With Owner Data (Very Fast).
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
User avatar
mk-soft
Always Here
Always Here
Posts: 6493
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: updated: Simple and useful form layout designer for DBase / SQLite and others

Post by mk-soft »

Axolotl wrote: Wed Jan 14, 2026 10:58 am Thanks again for sharing.

IDEA: I'm not sure if it's a rule, but I think it's good practice to include a brief history in the first post when updating.
Advantage: you/we can see the current status at a glance.
See as an example mk-soft's posts like Window ListIconGadget With Owner Data (Very Fast).
That's right. But since a certain level I take the development level I shorten it in the first post.
The update information can be read further in the following contributing also understandable.
Always with version information and changes.
With win API I sometimes also made changes that were taken back again. Go Back To Version.
My Projects ThreadToGUI / OOP-BaseClass / EventDesigner V3
PB v3.30 / v5.75 - OS Mac Mini OSX 10.xx - VM Window Pro / Linux Ubuntu
Downloads on my Webspace / OneDrive
Post Reply