Winforms Python

Hi,

Just an update for some fun I’ve been having with @BenRobinson_HB’s work on Winforms and python.

Winforms Example.dyn (19.9 KB)

I wanted tp be able to run a single Python script and include a dialogue…

Usually you would make an XAML code to define where everything goes and how it works, then you’d have a separate C# code to do all the heavy lifting.

A different option is to still use the XAML, but load it to Python using PresentationFramework in presentationframework, seemingly some limitations, but easier to work with.

I think the biggest problem for this method is that you can’t pass input updates from window to window, I suspect this is a Dynamo limitation, more than a Python limitation.

Ben’s work is found here: How to Build Graphical User Interface Nodes for Dynamo Using Python | Autodesk University

I left in the dropdown he made, I’ve also done some other things… Checkboxes, Text input &Text input warning… This is fun because there is no WInform just for number input, I tried to make a Dynamic warning which ran when you put in a character which failed a reg-ex check, but it wouldn’t dynamically update when you removed the illegal character.

There’re are lots of lines of code, but they are relatively simple… As Ben noted, there is very little Python documentation on this, so hopefully it is useful to expand it a bit.

I have kept the code as readable as possible. All comments welcomed!

You will need to change the paths to your own logos / bitmaps, otherwise it should just run. I tried to get bitmaps from a web address but I couldn’t get that working without saving locally (which seemed a bit rude)…

#thanks to Ben Robinson

import clr
import sys
pyt_path = r’C:\Program Files (x86)\IronPython 2.7\Lib’
sys.path.append(pyt_path)

    ### UI additional references ###

clr.AddReference(“System.Windows.Forms”)
clr.AddReference(“System.Drawing”)

from System.Windows.Forms import Application, Form, FormWindowState, Screen, Label, PictureBox, PictureBoxSizeMode, AnchorStyles, BorderStyle, ComboBox, ComboBoxStyle, FormBorderStyle, CheckBox, TextBox, TextBoxBase
from System.Windows.Forms import Button, LinkLabel, Panel, Button
from System.Drawing import Icon, Color, Font, Point, Size#, Image

#system location C:\Windows\Microsoft.NET\Framework64\v4.0.30319
import System.IO

    ### Define some variables ###

#these variables are static through the 2 windows
bitmapImage = System.Drawing.Bitmap(“C:\Me\Dynamo\WPF in Dynamo\” + “dyna-sco-logo.png”) #titlebar logo as bmp
titleIcon = Icon.FromHandle(bitmapImage.GetHicon()) #titlebar logo as icon
titleText = “Dimensioning” #text that appears in the GUI titlebar
btnHeight = 40 #declared here as used for btn locations
btnWidth = 120
spacing = 20 #spacing size for GUI elements to form a consistent border
fontMessage = Font("Helvetica ", 9)
fontCK = Font("Helvetica ", 8) #set Checkbox Font
winSize = Size(1000,600) #consistant window size

    ### Global defs ###

def button(txt, loc, clc):
btn = Button()
#btnCancel.Parent = self
btn.Text = txt
btn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right)
btn.Location = loc
btn.Click += clc
btn.Height = btnHeight
btn.Width = btnWidth
btn.BackColor = Color.FromArgb(220, 220, 220)
return btn

def logo(uiW, uiH):
logo = PictureBox()
bitmapImage = System.Drawing.Bitmap(“C:\Me\Dynamo\WPF in Dynamo\” + “dyna-sco-logo-text.png”)
logo.Image = bitmapImage
ratio = float(logo.Height)/ float(logo.Width) #needs to be a float as int will round to the nearest whole number
logo.Size = Size(220, 130*ratio) #fixed for consistancy
logo.Location = Point(spacing, (uiH - logo.Height)-spacing)
logo.SizeMode = PictureBoxSizeMode.Zoom # zooms the image to fit the extent
logo.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left) #anchor styles lock elements to a given corner of the GUI if you allow users change size
return logo

#Create a Class to define the first window

#create a instance of the form class called dimsionInputs.
#In Winforms, any window or a dialog is a Form.#

class dimsionInputs(Form):

def __init__(self): #the __init__ method inside a class is its constructor

    self.Text = titleText             
    self.Icon = titleIcon
    self.BackColor = Color.FromArgb(255, 255, 255)  
    self.WindowState = FormWindowState.Normal # set maximised minimised or normal size GUI
    self.CenterToScreen() #centres GUI to the middle of your screen 
    self.BringToFront() #brings the GUI to the front of all open windows
    self.Topmost = True #true to display the GUI infront of any other active forms
    self.Size = winSize #small enough to fit on most screens
    uiWidth = self.DisplayRectangle.Width    #get the size of the form to use to scale form elements
    uiHeight = self.DisplayRectangle.Height
    stHeight = uiHeight / 4.5 #the height at which the check & text boxes will start
    htSpacing = 50 #a standard spacing vertical gap betwen check & text boxes    
    self.FormBorderStyle = FormBorderStyle.FixedDialog      # fixed dialog stops the user from adjusting the form size. Recomended disabling this when testing to see if elements are in the wrong place.#
    
    ### Set Default Values ###
    
   #self.dropDownOutput = 'pick one'
    
    self.txtBoxOffOutput = '500'  #offset of dimension from wall
    self.txtBoxCoordOutput = 'Internal' #name of coordinate system to be used
    self.ckBoxESOutput = False  #External Side?
    self.ckBoxLWOutput = False  #Linked Walls?
    self.ckBoxAWOutput = False  #All Walls?
    self.ckBoxMDOutput = False  #Multiple dims per wall?
    self.messText = 'test'
    self.runNextOutput = False  #how we decide if we want the next window to fire#
    self.Controls.Add(logo(uiWidth, uiHeight)) #add logo
    

    #Create Main Window Text
    
    userMessage = Label()   #label displays texts
    font = fontMessage
    userMessage.Text = 'Automatic Dimensions for Linked or Project Walls'
    userMessage.Font = font
    userMessage.Location = Point(spacing, spacing)  #all location require a point object from system.Drawing to set the location.
    userMessage.Size = Size(uiWidth-(spacing*2),(uiHeight/4)-60)   #size the control with the width of the GUI to ensure it scales with different screen
    self.Controls.Add(userMessage)       #this adds control element to the GUI


    #combox drop down
    #cBox = ComboBox()   #dropdown control form
    #cBox.Location = Point(spacing,uiHeight/3)       
    #cBox.Width = uiWidth -(spacing*2)
   # cBox.Items.AddRange(tuple[(1,2,3,]))    # Adds an array of items to the list of items for a ComboBox.
   #cBox.DropDownStyle = ComboBoxStyle.DropDownList     #setting to dropdown list prevents users from being able to add aditional text values
    #cBox.SelectedIndexChanged += self.dropDownOutput  #.Click+= registers the press of the button to register the event handler and determine what action takes place when button clicked
    #self.Controls.Add(cBox)   


    ### CHECKBOXES ##
          
    #checkbox External Side
    ckBoxES = CheckBox()
    ckBoxES.Name="External Side"  
    ckBoxES.Text="External Side? (or internal side)"
    ckBoxES.Location = Point(spacing,stHeight)       
    ckBoxES.Width = uiWidth -(spacing*2)
    ckBoxES.Font = fontCK
    ckBoxES.Height = 40   
    ckBoxES.CheckStateChanged += self.ckBoxESChecked

    #checkbox Linked Walls 
    ckBoxLW = CheckBox()
    ckBoxLW.Name="Linked Walls"  
    ckBoxLW.Text="Linked Walls?"
    ckBoxLW.Location = Point(spacing,stHeight+htSpacing)       
    ckBoxLW.Width = uiWidth -(spacing*2)
    ckBoxLW.Font = fontCK
    ckBoxLW.Height = 40
    ckBoxLW.CheckStateChanged += self.ckBoxLWChecked

    
    #checkbox All Walls
    ckBoxAW = CheckBox()
    ckBoxAW.Name="All Walls"  
    ckBoxAW.Text="All Walls?"
    ckBoxAW.Location = Point(spacing,(stHeight+htSpacing*2))       
    ckBoxAW.Width = uiWidth -(spacing*2)
    ckBoxAW.Height = 40
    ckBoxAW.Font = fontCK
    ckBoxAW.CheckStateChanged += self.ckBoxAWChecked
    
    #checkbox MultiDims
    ckBoxMD = CheckBox()
    ckBoxMD.Name="Multi-Dims"  
    ckBoxMD.Text="Multi-Dims for each wall (for brick checking)"
    ckBoxMD.Location = Point(spacing,(stHeight+htSpacing*3))       
    ckBoxMD.Width = uiWidth -(spacing*2)
    ckBoxMD.Height = 40
    ckBoxMD.Font = fontCK
    ckBoxMD.CheckStateChanged += self.ckBoxMDChecked
    
    #add to Window
    self.Controls.Add(ckBoxES)
    self.Controls.Add(ckBoxLW)
    self.Controls.Add(ckBoxAW) 
    self.Controls.Add(ckBoxMD)  

    ###TEXTBOXES ##
 
    #TextBox Coordinate System      
    textBoxCoord = TextBox()
    textBoxCoord.Width = 300
    textBoxCoord.Text="Internal"
    textBoxCoord.Font = fontCK
    textBoxCoord.Location = Point(spacing + 400 ,(stHeight+htSpacing*4))
    textBoxCoord.TextChanged += self.txtBoxCoordChanged
    
    #TextBox Coordinate System - Label
    textBoxCoordLabel = Label()   #label displays texts
    textBoxCoordLabel.Text = "Coordinate System Name"
    textBoxCoordLabel.Width = 400
    textBoxCoordLabel.Font = fontCK
    textBoxCoordLabel.Location = Point(spacing,(stHeight+htSpacing*4))
    textBoxCoordLabel.Height = 50
    
    #add to Window
    self.Controls.Add(textBoxCoordLabel)  
    self.Controls.Add(textBoxCoord)


    
    #TextBox Offset        
    textBoxOffset = TextBox()
    textBoxOffset.Location = Point(spacing + 400,(stHeight+htSpacing*5))       
    textBoxOffset.Width = 300
    textBoxOffset.Text= "500"
    textBoxOffset.Name="Offset Distance"  
    textBoxOffset.Font = fontCK
    textBoxOffset.TextChanged += self.txtBoxOffChanged

           
    #TextBox Offset - Label
    textBoxOffsetLabel = Label()   #label displays texts
    textBoxOffsetLabel.Text = "Offset Distance"
    textBoxOffsetLabel.Width = 200
    textBoxOffsetLabel.Font = fontCK
    textBoxOffsetLabel.Height = 40
    textBoxOffsetLabel.Location = Point(spacing,(stHeight+htSpacing*5))
    #this is how you would do a margin
    #textBoxOffsetLabel.Margin = Padding(712,418,0,0) 
    
    #add to Window
    self.Controls.Add(textBoxOffsetLabel)  
    self.Controls.Add(textBoxOffset)

    #Buttons

    #Create Ok Button
    btnOkClick = self.okButtonPressed #register click by user
    btnOkLoc = Point(uiWidth - ((btnWidth * 2) + spacing + 30), uiHeight - (btnHeight + spacing))
    btnOk = button('OK', btnOkLoc, btnOkClick) 
    
    #Create Cancel Button
    btnCnclClick = self.CnlButtonPressed #register click by user
    btnCnclLoc = Point(uiWidth - (btnHeight + spacing + 90), uiHeight - (btnHeight + spacing ))
    btnCancel = button('Cancel', btnCnclLoc, btnCnclClick)
    
    #add to Window
    self.Controls.Add(btnOk)
    self.Controls.Add(btnCancel)   

    ###Return Info From User

#these defs are the mechanism by which we assess the info from the user
#self is the instance of the GUI form. Sender is the control/widget. args is the argument/event provided from the control

###dropDown ####
#def dropDownOutput(self, sender, args):
    self.dropDownOutput = sender.SelectedItem   #output the selected item.

### checkBoxes ###    
def ckBoxESChecked(self, sender, args):
    if sender.Checked:
        self.ckBoxESOutput = True
    #otherwise indeterminate states give erroneous results
    else:
        self.ckBoxESOutput = False 
        
def ckBoxLWChecked(self, sender, args):
    if sender.Checked:
        self.ckBoxLWOutput = True
    else:
        self.ckBoxLWOutput = False

def ckBoxAWChecked(self, sender, args):
    if sender.Checked:
        self.ckBoxAWOutput = True
    else:
        self.ckBoxAWOutput = False

def ckBoxMDChecked(self, sender, args):
    if sender.Checked:
        self.ckBoxMDOutput = True
    else:
        self.ckBoxMDOutput = False
        
       
### textBoxes ###        
def txtBoxOffChanged(self, sender, args):     
    self.txtBoxOffOutput = sender.Text
            
    
def txtBoxCoordChanged(self, sender, args):
    self.txtBoxCoordOutput = sender.Text 

### buttons ###
def okButtonPressed(self, sender, args):
    numErrorProvider = System.Windows.Forms.ErrorProvider()
    try:
        int(self.txtBoxOffOutput)
        self.Close()    #trigger to close the GUI when button is pressed
        self.runNextOutput = True #if the ok button is pressed set runNextOutput as True
    except ValueError:        
        numErrorProvider.SetError(sender, 'Offset must be a number')
def CnlButtonPressed(self, sender, args):
    self.Close()
    self.runNextOutput = False #if the cancel button is pressed set runNextOutput as False 


    ###Create a Class to define the second window ####

class followForm(Form):

def __init__(self): #the __init__ method inside a class is its constructor
    self.Text = titleText             
    self.Icon = titleIcon        
    self.Size = winSize
    self.CenterToScreen()
    self.FormBorderStyle = FormBorderStyle.FixedDialog        
    self.runNextOutput = False  #how we decide if we want the next window to fire 
    uiWidth = self.DisplayRectangle.Width    #get the size of the form to use to scale form elements
    uiHeight = self.DisplayRectangle.Height
    self.Controls.Add(logo(uiWidth, uiHeight)) #add logo       

#############--------------------------#############

    ###Buttons ## 

    #Create Ok Button
    btnOkClick = self.okButtonPressed #register click by user
    btnOkLoc = Point(uiWidth - ((btnWidth * 2) + spacing + 30), uiHeight - (btnHeight + spacing))
    btnOk = button('OK', btnOkLoc, btnOkClick) 

    #Create Cancel Button
    btnCnclClick = self.CnlButtonPressed #register click by user
    btnCnclLoc = Point(uiWidth - (btnHeight + spacing + 90), uiHeight - (btnHeight + spacing ))
    btnCancel = button('Cancel', btnCnclLoc, btnCnclClick)
    
    #add to Window
    self.Controls.Add(btnOk)
    self.Controls.Add(btnCancel)  

    ###Create Second Window Text ###
           
    userMessageScnd = Label()   #label displays texts
    font = fontMessage
    dimsionInputs()
    #message(ckBoxLWChecked, ckBoxAWChecked)
    userMessageScnd.Text = 'runNextOutput = ' + str(dimForm.runNextOutput) + '''\n\nThis is a contradiction! \n\nThis window only fired because runNextOutput was changed to True by the previous window! \n\nSo, sadly, it seems we can\'t link a series of windows driven by dynamic information without daisy-chaining python nodes together'''
    #userMessageScnd.Text = dimsionInputs().messText
    userMessageScnd.Font = font
    userMessageScnd.Location = Point(spacing, spacing)  #all location require a point object from system.Drawing to set the location.
    userMessageScnd.Size = Size(uiWidth-(spacing*2),(uiHeight))   #size the control with the width of the GUI to ensure it scales with different screen
    self.Controls.Add(userMessageScnd)       #this adds control element to the GUI

    ###Return Info From User
    
###buttons ###
def okButtonPressed(self, sender, args):
    self.Close()    #trigger to close the GUI when button is pressed
    self.runNextOutput = True #if the ok button is pressed set runNextOutput as True#
def CnlButtonPressed(self, sender, args):
    self.Close()

    ###And Run ##

dimForm = dimsionInputs()
followForm = followForm()

Application.Run(dimForm)

if dimForm.runNextOutput:
Application.Run(followForm)

OUT = dimForm.ckBoxESOutput, dimForm.ckBoxLWOutput, dimForm.ckBoxAWOutput, dimForm.ckBoxMDOutput, dimForm.txtBoxOffOutput, dimForm.txtBoxCoordOutput,

Cheers,

Mark

15 Likes

This is WinForms not WPF :stuck_out_tongue_winking_eye:

3 Likes

Argh sorry Thomas, will edit title :slight_smile:

2 Likes

No problem mate, thanks for sharing, its a great resource.

2 Likes

Oddly Chrome displays the code strangely but it looks fine on my phone… I’ve added a dyn.

1 Like

looking good!
I need to pass on credit to @Mostafa_El_Ayoubi I started looking into this after using the DataShapes packages which is great!

Doing Winforms in python gave me the skills to take the jump to C# and i will say its a lot bloody easier to do in Visual Studio. Having said that I still build winform nodes / use DataShapes if I am building a quick Dynamo script for someone in the office. Once you have the fundamental elements set up its quick to duplicate.

4 Likes