Forum Archive

Generate UI code from .pyui

Robert_Tompkins

Hey guys, I’m just posting this here so other people can use it if they made the same mistake I did.
I wrote many programs and later built UI files in Pythonista to complement them. However, when I wrote a math program with UI for my daughter, every change I made required me to share BOTH the .py and .pyui files in order for her to use the updated version.

So I wrote this function/method (that is still a work in progress) to simplify this process.
It essentially takes a UI.View() object and generates copy/paste-able code that can be pasted into your .py file to setup and configure the view so you don’t have to load a .pyui file.

Small example of code produced from simple .pyui below. Below that is the function.
3 ui.TextField()
3 ui.Label()
2 ui.Button()

#_______________________________________________

acInputField = ui.TextField()
calculateDistanceButton = ui.Button()
label2 = ui.Label()
dcInputField = ui.TextField()
label1 = ui.Label()
arcDistanceOutputField = ui.TextField()
label3 = ui.Label()
resetFieldsButton = ui.Button()
acInputField.name = 'acInputField'
acInputField.x = 175.0
acInputField.y = 250.0
acInputField.width = 150.0
acInputField.height = 32.0
acInputField.background_color = (0.250545, 0.250545, 0.250545, 1.0)
acInputField.border_width = 0.0
acInputField.tint_color = (0.431645, 0.431645, 0.431645, 1.0)
acInputField.font = ('.SFUI-Regular', 17.0)
acInputField.text = ''
acInputField.autoresizing = 'LRTB'
acInputField.alignment = 0
acInputField.flex = 'LRTB'
acInputField.corner_radius = 0.0
acInputField.content_mode = 0
calculateDistanceButton.name = 'calculateDistanceButton'
calculateDistanceButton.action = calculateDistanceTapped
calculateDistanceButton.x = 75.0
calculateDistanceButton.y = 350.0
calculateDistanceButton.width = 250.0
calculateDistanceButton.height = 75.0
calculateDistanceButton.background_color = (0.401961, 0.401961, 0.401961, 1.0)
calculateDistanceButton.border_width = 0.0
calculateDistanceButton.tint_color = (0.25, 1.0, 0.25, 1.0)
calculateDistanceButton.title = 'Calculate'
calculateDistanceButton.font = ('.SFUI-Semibold', 20.0)
calculateDistanceButton.autoresizing = 'TB'
calculateDistanceButton.flex = 'TB'
calculateDistanceButton.corner_radius = 15.0
calculateDistanceButton.content_mode = 0
label2.name = 'label2'
label2.x = 25.0
label2.y = 250.0
label2.width = 125.0
label2.height = 32.0
label2.background_color = (0.251634, 0.251634, 0.251634, 1.0)
label2.border_width = 0.0
label2.tint_color = (0.25, 0.6084999999999998, 1.0, 1.0)
label2.font = ('.SFUI-Regular', 18.0)
label2.text = 'VAC'
label2.autoresizing = 'LRTB'
label2.alignment = 1
label2.flex = 'LRTB'
label2.corner_radius = 0.0
label2.line_break_mode = 4
label2.content_mode = 7
label2.number_of_lines = 0
dcInputField.name = 'dcInputField'
dcInputField.x = 175.0
dcInputField.y = 210.0
dcInputField.width = 150.0
dcInputField.height = 32.0
dcInputField.background_color = (0.250545, 0.250545, 0.250545, 1.0)
dcInputField.border_width = 0.0
dcInputField.tint_color = (0.431645, 0.431645, 0.431645, 1.0)
dcInputField.font = ('.SFUI-Regular', 17.0)
dcInputField.text = ''
dcInputField.autoresizing = 'LRTB'
dcInputField.alignment = 0
dcInputField.flex = 'LRTB'
dcInputField.corner_radius = 0.0
dcInputField.content_mode = 0
label1.name = 'label1'
label1.x = 25.0
label1.y = 210.0
label1.width = 125.0
label1.height = 32.0
label1.background_color = (0.251634, 0.251634, 0.251634, 1.0)
label1.border_width = 0.0
label1.tint_color = (0.25, 0.6084999999999998, 1.0, 1.0)
label1.font = ('.SFUI-Regular', 18.0)
label1.text = 'VDC'
label1.autoresizing = 'LRTB'
label1.alignment = 1
label1.flex = 'LRTB'
label1.corner_radius = 0.0
label1.line_break_mode = 4
label1.content_mode = 7
label1.number_of_lines = 0
arcDistanceOutputField.name = 'arcDistanceOutputField'
arcDistanceOutputField.x = 175.0
arcDistanceOutputField.y = 290.0
arcDistanceOutputField.width = 150.0
arcDistanceOutputField.height = 32.0
arcDistanceOutputField.background_color = (0.248366, 0.248366, 0.248366, 1.0)
arcDistanceOutputField.border_width = 0.0
arcDistanceOutputField.tint_color = (0.32543625, 0.32543625, 0.32543625, 1.0)
arcDistanceOutputField.font = ('.SFUI-Regular', 17.0)
arcDistanceOutputField.text = ''
arcDistanceOutputField.autoresizing = 'LRTB'
arcDistanceOutputField.alignment = 0
arcDistanceOutputField.flex = 'LRTB'
arcDistanceOutputField.corner_radius = 0.0
arcDistanceOutputField.content_mode = 0
label3.name = 'label3'
label3.x = 25.0
label3.y = 290.0
label3.width = 125.0
label3.height = 32.0
label3.background_color = (0.248366, 0.248366, 0.248366, 1.0)
label3.border_width = 0.0
label3.tint_color = (0.25, 0.6084999999999998, 1.0, 1.0)
label3.font = ('.SFUI-Regular', 18.0)
label3.text = 'Arc Distance'
label3.autoresizing = 'LRTB'
label3.alignment = 1
label3.flex = 'LRTB'
label3.corner_radius = 0.0
label3.line_break_mode = 4
label3.content_mode = 7
label3.number_of_lines = 0
resetFieldsButton.name = 'resetFieldsButton'
resetFieldsButton.action = resetFieldsTapped
resetFieldsButton.x = 75.0
resetFieldsButton.y = 433.0
resetFieldsButton.width = 250.0
resetFieldsButton.height = 75.0
resetFieldsButton.background_color = (0.401961, 0.401961, 0.401961, 1.0)
resetFieldsButton.border_width = 0.0
resetFieldsButton.tint_color = (0.25, 1.0, 0.25, 1.0)
resetFieldsButton.title = 'Reset Fields'
resetFieldsButton.font = ('.SFUI-Semibold', 20.0)
resetFieldsButton.autoresizing = 'TB'
resetFieldsButton.flex = 'TB'
resetFieldsButton.corner_radius = 15.0
resetFieldsButton.content_mode = 0
fullView.add_subview(acInputField)
fullView.add_subview(calculateDistanceButton)
fullView.add_subview(label2)
fullView.add_subview(dcInputField)
fullView.add_subview(label1)
fullView.add_subview(arcDistanceOutputField)
fullView.add_subview(label3)
fullView.add_subview(resetFieldsButton)
#_______________________________________________

```
def generateUserInterfaceCode(viewObject):
fullView = viewObject
listOfTypes = []
listOfChildren = []
dictOfObjects = {}
debugDict = {}
listOfObjectTypes = []
for object in fullView.subviews:
objectType = object._pyui['class']
text = None
action = None
autoresizing = None
alignment = None
flex = None
corner_radius = None
border_color = None
border_width = None
background_color = None
tint_color = None
title = None
font = None
continuous = None
content_mode = None
line_break_mode = None
number_of_lines = None

    name = object.name
    x = object.x
    y = object.y
    width = object.width
    height = object.height

    try:
        tint_color = object.tint_color
    except AttributeError:
        None
    try:
        border_color = object.border_color
    except AttributeError:
        None
    try:
        border_width = object.border_width
    except AttributeError:
        None
    try:
        title = f"'{object.title}'"
    except AttributeError:
        None
    try:
        text = f"'{object.text}'"
    except AttributeError:
        None
    try:
        font = object.font
    except AttributeError:
        None
    try:
        autoresizing = f"'{object.autoresizing}'"
    except AttributeError:
        None
    try:
        flex = f"'{object.flex}'"
    except AttributeError:
        None
    try:
        alignment = object.alignment
    except AttributeError:
        None
    try:
        corner_radius = object.corner_radius
    except AttributeError:
        None
    try:
        background_color = object.background_color
    except AttributeError:
        None
    try:
        continuous = object.continuous
    except AttributeError:
        None
    try:
        if object.action != None:
            action = object.action
            action = str(action)
            #action = action.lstrip('<function ')
            actionList = action.split(' ')
            action = actionList[1]
            if object.name == "difficultySlider":
                action = f"difficultyChanged"
            else:
                action = f"{action}Tapped"
    except AttributeError:
        None
    try:
        line_break_mode = object.line_break_mode
    except AttributeError:
        None
    try:
        content_mode = object.content_mode
    except AttributeError:
        None
    try:
        number_of_lines = object.number_of_lines
    except AttributeError:
        None

    dictOfObjects[name] = {
        'type' : objectType,
        'action' : action,
        'x' : x,
        'y' : y,
        'width' : width,
        'height' : height,
        'background_color' : background_color,
        'border_color' : border_color,
        'border_width' : border_width,
        'tint_color' : tint_color,
        'title' : title,
        'font' : font,
        'text' : text,
        'autoresizing' : autoresizing,
        'alignment' : alignment,
        'flex' : flex,
        'corner_radius' : corner_radius,
        'continuous' : continuous,
        'line_break_mode' : line_break_mode,
        'content_mode' : content_mode,
        'number_of_lines' : number_of_lines}

    continuous = None
    title = None
    text = None

    listOfChildren.append(object)

print(f"#{'_' * 47}\n")
for k, v in dictOfObjects.items():
    print(f"{k} = ui.{v['type']}()")
for k, v in dictOfObjects.items():
    print(f"{k}.name = '{k}'")
    for key, value in v.items():
        if value != None and key != 'type':
            print(f"{k}.{key} = {value}")

for k, v in dictOfObjects.items():
    if k != 'type':
        print(f"fullView.add_subview({k})")
print(f"#{'_' * 47}\n") ```
Bambla

Just great I wish I had known that before. 😍

ccc

Just change the file extension… my_file.pyui —> my_file.json and you have a valid and easy to transfer json file. Send the .py and .json files to your recipient and request that they rename my_file.json —> my_file.pyui before running the .py file.

You could even automate this rename process just before calling ui.load_view() like this:

from pathlib import Path

import ui

json_file_path = Path(__file__).with_suffix(".json")
pyui_file_path = Path(__file__).with_suffix(".pyui")
if json_file_path.is_file() and not pyui_file_path.is_file():
    json_file_path.rename(pyui_file_path)

v = ui.load_view()
v.present('sheet')

Also, see https://docs.python.org/3/library/json.html

ccc

Perhaps even slicker, just allow your recipient to load the ui directly from my_file.json

try:
    v = ui.load_view()
except FileNotFoundError:
    v = ui.load_view(__file__[:-2] + "json")
v.present('sheet')
Robert_Tompkins

@ccc ... Seriously? Lol. I open every file I’m curious about via Notepad++ just to see what’s goin on..but that’s at work/on a PC.
Without Notepad++ the thought never crossed my mind that maybe the .pyui file contains easily extractable/readable data. It’s even in dict/json format -.-
Well, thanks for the info. I’ll mess around with it and see if I can modify the function to serialize/deserialize the json and generate the copy/paste-able code still. This opens a world of options. Thanks!

Bambla

Being a lazy bone, I will probably use ccc‘s json workaround.

You still have more than one .py-file, but no more need for others to manually rename files.

ccc

You still have more than one .py-file

Only the .py files that call ui.load_view() need the workaround.

cvp

@Robert_Tompkins said

via Notepad++

Staying in Pythonista, you can even rename the .pyui into .txt so you can edit it as text only to see its content

And if you want to send only one file, you can always use load_view_str by copying/pasting the content into the .py

import ui

pyui_json = '''
[
  {
    "nodes" : [
      {
        "nodes" : [

        ],
        "frame" : "{{80, 104}, {80, 32}}",
        "class" : "Button",
        "attributes" : {
          "uuid" : "B2F85C43-43EC-4980-AABD-045802F910A3",
          "frame" : "{{80, 104}, {80, 32}}",
          "title" : "Button",
          "class" : "Button",
          "name" : "button1",
          "font_size" : 15
        },
        "selected" : true
      }
    ],
    "frame" : "{{0, 0}, {240, 240}}",
    "class" : "View",
    "attributes" : {
      "enabled" : true,
      "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)",
      "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)",
      "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)",
      "flex" : ""
    },
    "selected" : false
  }
]
'''
v = ui.load_view_str(pyui_json)
v.present()
ccc

rename the .pyui into .txt so you can edit it as text only to see its content

Renaming from .pyui to .json also allows you to edit the file in Pythonista with the added advantage of syntax highlighting.