# Copyright (c) 2014, Varian Medical Systems, Inc. (VMS)
# All rights reserved.
#
# Veritas is an open source tool for TrueBeam Developer Mode provided by Varian Medical Systems, Palo Alto.
# It lets users generate XML beams without assuming any prior knowledge of the underlying XML-schema rules.
# This version is based on the schema for TrueBeam 2.0.
#
# Veritas is licensed under the Varian Open Source License.
# You may obtain a copy of the License at:
#
# http://radiotherapyresearchtools.com/license/
#
# For questions, please send us an email at: TrueBeamDeveloper@varian.com
#
# Developer Mode is intended for non-clinical use only and is NOT cleared for use on humans.
# Created on: 11:11:18 AM, Jul 7, 2014
# Author: Pankaj Mishra
#
# Update 7/29/14 - Removed hardcoded paths and win32api dependence -CLW
# Update 7/28/14 - Added 'electron' option for energy type - JDeMarco
# Update 9/16/14 - Updated to generates XML beam for TrueBeam 2.0
# - Window based api is removed to make this version agnostic to
# - under lying OS
# - Removed hard-coded output XML beam file name
# Update 10/7/14 - Included IEC to Varian scale conversion in dicom2xml converter
# Update 5/12/15 - Major Documentation update - Alan Chang
# Update 4/13/16 - Added new functionality 'Dicom Tree' - Nilesh Gorle
# *************************************
# This file is the entrypoint to the program.
# In general, any given class has the following structure:
# 1) Each class, in the root folder corresponds logically to
# a single window in the main application
# 2) The visual layout and UI elements are specified by an import
# of form ui_xxxxx where xxxx is the name of the class
# 3) The main dataflow is mediated by the cp_data variable, which
# is used as a container to pass information between windows
# 4) The class itself handles the semantics of what UI elements
# map to what actions and what data passes through to cp_data
# 5) Any instance attributes inside a class is used as bookkeeping
# to keep track of data before it gets sent to cp_data
# The above will be reproduced in the preface document, and is included
# to be an inline reference to the structure.
# Up to date as of May, 12 2015.
# %%%
# The main work flow of mainwindow is the following:
# It starts out with an empty CPData instance in self.cp_data
# Which then gets populated, either as a result of a child
# window providing data, or via an import from a file.
# When the CPData is populated, it can then be consumed
# via cp_data.create_xml() to provide the root of an xml tree (see self.plotXML and
# openBeamonHeader)
# Any non-responsive or buggy UI elements will be here
# Any xml import or export errors will be in CPData
# Any problems in sub windows will be in the respective files
# *************************************
# These imports are either provided by Pyside for manipulating UI elements
# Or generated via QtDesigner + pyside-uic
# Look in ui_files for UI element names
try:
from PySide.QtGui import (QMainWindow, QDialog, QIcon, QFileDialog, QMessageBox,
QApplication, QLabel, QPushButton, QTreeView, QFrame,
QStandardItemModel, QStandardItem, QAbstractItemView,
QStandardItem, QHeaderView, QSizePolicy, QTextBrowser,
QVBoxLayout,QWidget, QImage, QPixmap, QScrollArea)
from PySide.QtCore import QCoreApplication, QRect, Qt
# The following imports are for plotting
import matplotlib
matplotlib.use('Qt4Agg')
matplotlib.rcParams['backend.qt4'] = 'PySide'
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
except ImportError:
#print('Pyside not getting imported')
pass
from UI import ui_mainWindow, ui_licenseDlg
# Modules containing other sub windows
# Any dialog or windows generated from those subwindows
# are also defined here
import imaging, beamon
# Container that passes data to and from windows, also responsible
# for data integrity and import/export of xml
# Look here for extending data type or bugs in XML specification
from models.CPData import CPData
# Utility helper functions
import sys, os, subprocess, shutil
from xml.etree import ElementTree as ET
from utils import dicom2xml, plotXML, SetBeam20, dcm_qt_tree #setBeam
DEFAULT_XML_FILE = os.path.join('output','output.xml')
[docs]class MainWindow(object):#QMainWindow, ui_mainWindow.Ui_MainWindow):
'''The main window class is the entrance point for both DICOM-RT based
XML generation/modification as well as for the XML-from-scratch.
'''
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.setWindowTitle("Veritas: TrueBeam Developer Mode")
self.setWindowIcon(QIcon('truebeam2.jpg'))
self._init_beam_on()
self._init_imaging()
self._init_text_editor_load()
self._init_plot()
self._init_menu()
self.dirty = False
self.XMLfile = DEFAULT_XML_FILE
# Activates on pressing the BeamON button
[docs] def openBeamonHeader(self, cpdata=CPData()):
'''Open the header part of XML generation. Header basically refers to
the header as well as part of the control point 0.
:param cpdata
cpdata=None: build XMLbeam file from scratch
cpdata=cpdata: process a previously generated XMLbeam file'''
if cpdata is None:
self.cpdata = CPData()
self.cpGUI = beamon.beamonHeader(cpdata=self.cpdata)
self.cpGUI.exec_()
self.cpHeader = self.cpGUI.cpdata.cpHeader
#=======================================================================
# When the doneButton is pressed from the control point
# exec_() generates int 1 as a 'signal'. After receiving the signal
# the control point window 1 is closed and XML file is generated
#=======================================================================
if self.cpHeader is not None: # To exclude close button and 'cancel' scenario
if(self.cpGUI.cpWindow.exec_() == 1):
self.cpdata = self.cpGUI.cpWindow.get_data()
# Prompt the user for choosing output file name
qout = QFileDialog.getSaveFileName(self, "Select output XML file",
dir=self.XMLfile,
filter="XML Files (*.xml);;All Files (*.*)")
self.XMLfile = qout[0]
self.dirty = True # Set the flag for saving the XML file
# MISLEADING NAME. SetBeam20.parse also writes to self.XMLfile!
root = self.cpdata.create_xml()
SetBeam20.parse(root, self.XMLfile, True)
self.displayOutput(self.XMLfile)
# Activates on pressing Imaging Button
[docs] def openImagingWindow(self):
'''
Opens the imaging window, sending it data about
Whether inside treatment or outside treatment is wanted,
Based on the dropdown menu below the Imaging button.
'''
image_type = self.imagingTypeBox.currentText()
self.imagingGui = imaging.cpImage(None, '', image_type) # Instantiate an imaging window
self.imagingGui.openImaging() # Open the imaging window
self.imagingGui.exec_()
self.XMLfile = self.imagingGui.xml_file
self.displayOutput(self.XMLfile)
# Activates on pressing Plot Axes button
[docs] def plotXML(self):
'''
Plots the XML displayed, using the two dropdown
boxes + potentially the mlc text.
Majority of heavy lifting happens inside the plotXML.plot_fig function.
'''
x_axis = self.xAxisBox.currentText()
y_axis = self.yAxisBox.currentText()
try:
root = ET.parse(self.XMLfile)
except TypeError:
self._no_xml_defined()
self.openXMLPlan()
return
mlc_x = None
mlc_y = None
if self.mlcLeafX.text():
mlc_x = int(self.mlcLeafX.text())
x_axis = x_axis[-1]
# assumes dropdown text is of format MLC A or MLC B
if self.mlcLeafY.text():
mlc_y = int(self.mlcLeafY.text())
y_axis = y_axis[-1]
# assumes dropdown text is of format MLC A or MLC B
# Plot if all the data is correctly initialized, otherwise throw an error dialog
try:
self.ax = plotXML.plot_fig(self.ax, root, x_axis, y_axis, mlc_x, mlc_y)
self.canvas.draw()
self.canvas.show()
except plotXML.AxisError:
self._invalid_axis_box()
return
except AttributeError:
self._improper_x_axis()
return
# Activates on pressing Open XML File button
def openTextEditor(self):
'''
Open the final xml file in a text editor.
'''
#CW Note - Use a generic opener.
fileName = self.XMLfile
self.systemOpenFile(fileName)
# Activates on pressing "Open" icon or doing File > Open Plan
def openXMLPlan(self):
'''
Parse an existing XMl plan and display the file in header and subsequent
control point windows. This function can be launched if an xml file already
exists or an xml file has been generated from a DICOM file using Dcm2Xml function
'''
fileObj = QFileDialog.getOpenFileName(self, "Choose an .xml file", dir="input", filter="Text files (*.xml)")
fileName = fileObj[0]
if not os.path.isfile(fileName):
QMessageBox.information(self, "XML file", '''No XML file selected''', QMessageBox.Ok)
return
self.XMLfile = fileName
self.loadFile(fileName)
self.displayOutput(fileName)
# Activates on pressing Dcm2xml button
[docs] def dcm2xml(self):
'''
Calls DICOM-RT to XML conversion function.
A message box conveys the completion of XML file generation.
Most logic occurs inside the dicom2xml function
'''
# Prompt the user to select a file
fileObj = QFileDialog.getOpenFileName(self, "Choose a DICOM-RT file", dir="input", filter="Text files (*.DCM; *.dcm)")
fileName = fileObj[0]
# Let the user select the location to save the file
qout = QFileDialog.getSaveFileName(self,"Save output XML file",dir=self.XMLfile,filter="XML Files (*.xml);;All Files (*.*)")
outfile = qout[0]
# Cancel if nothing changes
if outfile == '':
QMessageBox.information(self, "DICOM conversion cancelled", 'DICOM conversion was cancelled', QMessageBox.Ok)
return
self.XMLfile = outfile
# Inform the user that the XML file has been generated
if os.path.isfile(fileName):
x = dicom2xml.Dicom2Xml(fileName, self.XMLfile)
x.dicomToDataset()
x.extractControlPoints()
# Message that a specific file has been generated
QMessageBox.information(self, "DICOM to XML conversion", '''XML file has been generated''', QMessageBox.Ok)
# Set the file generated bit to true
self.dirty = True
self.loadFile(outfile)
self.displayOutput(outfile)
else:
# No DICOM file has been selected
QMessageBox.information(self, "DICOM to XML conversion", '''No DICOM file selected''', QMessageBox.Ok)
[docs] def loadFile(self, fileName):
'''
Calls beamonHeader to load an existing XML file in
the Control point windows
:param fileName:
The file is read as an xml tree and then imported into the cpdata structure
'''
tree = ET.parse(fileName)
root = tree.getroot()
self.cpdata.import_xml(root)
# Activates on Help > About or Ctrl+A
def aboutVeritas(self):
'''
Print one line description, research only message line
and contact email in a message
'''
QMessageBox.about(self,"About Veritas",
"""<b> Veritas v 1.0 </b>
<p>Veritas is an open source tool for TrueBeam
Developer Mode provided by Varian Medical Systems, Palo Alto.
<p>It lets users generate XML beams without assuming any
prior knowledge of the underlying XML-schema rules
<p>This version is based on the schema for TrueBeam 1.5 and 1.6
<p>For questions, please send us an email at: TrueBeamDeveloper@varian.com
<p> <b> Developer Mode is intended for non-clinical use only
and is NOT cleared for use on humans </b> """)
# Activates on Help > Documentation or Ctrl+D
def viewManual(self):
'''
Display the TrueBeam Developer Mode manual in acrobat reader
'''
fileName = 'TrueBeamDeveloperModeManual_V_0.2.0_03_2012.pdf'
self.systemOpenFile(fileName)
# Activates on Help > License or Ctrl+L
def viewLicense(self):
'''
Display Varian open source (VOS) license in acrobat reader
'''
fileName = 'Veritas_License.pdf'
self.systemOpenFile(fileName)
def fileSave(self):
pass
# Activates on File > Save and pressing the Save button
def fileSaveAs(self):
'''
Save copy of an already existing XML file
'''
# No XML beam created
if self.dirty is False:
QMessageBox.information(self, "Save XML file", '''No output XML file exists''', QMessageBox.Ok)
return
# Let the user select the location to save the file
qout = QFileDialog.getSaveFileName(self, "Save output XML file",
dir = self.XMLfile, filter = "XML Files (*.xml);;All Files (*.*)")
# Save the XML file
# Copy2 save file content, its meta data as well as permissions
try:
shutil.copy2(self.XMLFile, qout[0])
# In case source or dest are same file
except shutil.Error as e:
print("Error: %s" % e)
# In case source or dest file doesn't exist
except IOError as e:
print("Error: %s" % e)
# What to do when main window closes
def closeEvent(self, event):
'''
Double check with the user that he/she wants to quit Veritas
:param event:
'''
reply = QMessageBox.question(self, "Veritas",
"Are you sure you want to quit?",
QMessageBox.Yes|QMessageBox.No)
if reply == QMessageBox.Yes:
event.accept()
elif reply == QMessageBox.No:
event.ignore()
# --- __init__ private helpers
def _init_beam_on(self):
self.beamonButton.clicked.connect(self.openBeamonHeader) # Connect to the beam on functionality
self.cpHeader = dict() # A set of attributes for data
self.cpControlPoints = list()
self.cpdata = CPData()
def _init_imaging(self, ):
self.imagingButton.clicked.connect(self.openImagingWindow)
def _init_text_editor_load(self):
"""Open XML file button """
self.xmlTextEdit.clicked.connect(self.openTextEditor)
def _init_plot(self):
"""Plot Axes button"""
fig = Figure(figsize=(5.0,4.0), dpi=72, facecolor=(1,1,1), edgecolor=(1,1,1))
self.ax = fig.add_subplot(111)
self.ax.hold(False)
self.canvas = FigureCanvas(fig)
self.vbox.addWidget(self.canvas)
self.plotAxes.clicked.connect(self.plotXML)
self.xAxisBox.currentIndexChanged.connect(self.display_mlc_x)
self.display_mlc_x()
self.yAxisBox.currentIndexChanged.connect(self.display_mlc_y)
self.display_mlc_y()
def display_mlc_x(self):
displayed_text = self.xAxisBox.currentText()
if displayed_text == 'Mlc A' or displayed_text == 'Mlc B':
self.leafXLabel.show()
self.mlcLeafX.show()
else:
self.leafXLabel.hide()
self.mlcLeafX.hide()
self.mlcLeafX.setText('')
def display_mlc_y(self):
displayed_text = self.yAxisBox.currentText()
if displayed_text == 'Mlc A' or displayed_text == 'Mlc B':
self.leafYLabel.show()
self.mlcLeafY.show()
else:
self.leafYLabel.hide()
self.mlcLeafY.hide()
self.mlcLeafY.setText('')
def dicomtree(self):
dtree_viewer = DicomTree(self)
dtree_viewer.dicomtree()
def _init_menu(self):
self.actionOpenPlan.triggered.connect(self.openXMLPlan)
self.actionCreatePlan.triggered.connect(lambda: self.openBeamonHeader(cpdata=None))
self.actionDcm2xml.triggered.connect(self.dcm2xml)
self.actionDicomTree.triggered.connect(self.dicomtree)
self.actionSave.triggered.connect(self.fileSaveAs)
self.actionSaveAs.triggered.connect(self.fileSaveAs)
self.actionExit.triggered.connect(self.close)
self.actionAbout.triggered.connect(self.aboutVeritas)
self.actionDocuments.triggered.connect(self.viewManual)
self.actionLicense.triggered.connect(self.viewLicense)
# --- End __init__ private helpers
# --- private helper that displays output xml
def displayOutput(self, fname):
'''
Read the XML file line-by-line and display it on the main window text-browser
Note: '<' and '>' needs to be replaced by < and > to display the XML tags correctly
This is important in case of 'bank B' which is otherwise mistaken as a 'bold' html tag.
:param fname:
'''
# Start with a clear browser display
# Also, cleans up the previous window display
self.textBrowser.clear()
# Read in xml file
xmlFile = open(fname, "r")
outputStr = xmlFile.readlines()
xmlFile.close()
# Replace '<' with '<' and '>' with '>'.
# This is done to avoid confusion.
# e.g., <B> mlc xml tag can be interpreted as bold
for line in outputStr:
line = line.replace("<","<")
line = line.replace(">",">")
line = line + '\n'
self.textBrowser.append('%s' % line)
self._change_button_color()
def _change_button_color(self):
self.beamonButton.setStyleSheet("background-color: teal")
# --- end displayOutput helper
# --- plotXML private helpers
def check_mlc_range(self, leaf_box):
if leaf_box.isHidden():
return True
try:
mlc_no = int(leaf_box.text())
if 60 >= mlc_no >= 1:
return True
else:
QMessageBox.warning(self,
u"Invalid MLC value",
u"MLC must be in interval [1,60]",
QMessageBox.Ok,
QMessageBox.NoButton)
except ValueError:
QMessageBox.warning(self,
u"Invalid MLC value",
u"MLC must be an integer",
QMessageBox.Ok,
QMessageBox.NoButton)
finally:
leaf_box.setText('')
return False
def _no_xml_defined(self):
QMessageBox.warning(self,
u"No XML to plot!",
u"Please open or construct and XML file first.",
QMessageBox.Ok,
QMessageBox.NoButton)
def _invalid_axis_box(self):
msgBox = QMessageBox()
msgBox.setWindowTitle('Veritas plotting error')
msgBox.setText("Select a valid axis type.")
msgBox.exec_()
def _improper_x_axis(self):
QMessageBox.warning(self,
u"Missing x-axis value",
u"XML file does not have x-axis properly defined!\n"\
"Select another axis or add more points to x-axis.",
QMessageBox.Ok,
QMessageBox.NoButton)
# -- end plotXML private helpers
@staticmethod
def systemOpenFile(fileName):
'''
Should hopefully be replaced by a 'real' cross-platform file-open at some point.
Added by CLW on 6/29/14
'''
try:
os.startfile(fileName)
except:
if sys.platform == "darwin":
subprocess.Popen(["open", fileName])
else:
subprocess.Popen(["xdg-open", fileName])
class LicenseAgreement(object):#QDialog, ui_licenseDlg.Ui_licenseDlg):
'''
Show the license window before the application starts
'''
def __init__(self, parent=None):
super(LicenseAgreement, self).__init__(parent)
self.setupUi(self)
self.setWindowTitle("Veritas: License Agreement")
self.setModal(True)
self.acceptButton.clicked.connect(self.startVeritas)
self.cancelButton.clicked.connect(self.cancelVeritas)
def startVeritas(self):
'''
User accepted the license agreement and is allowed to proceed
'''
self.mainwindow = MainWindow()
self.hide() # Hide is used as close calls the close event function
self.mainwindow.show()
def cancelVeritas(self):
'''
user decides not to proceed
'''
self.close()
def closeEvent(self, event):
'''
Double check with the user.
Also, alerts user in case he/ she accidentally presses the
close button
:param event:
'''
reply = QMessageBox.question(self, "Veritas", "Are you sure you want to quit?", QMessageBox.Yes|QMessageBox.No)
if reply == QMessageBox.Yes:
event.accept()
elif reply == QMessageBox.No:
event.ignore()
class Dicom_Dialog(object):
def setupUi(self, Dialog):
from PySide.QtCore import QMetaObject, QRect
Dialog.setObjectName("Dialog")
Dialog.resize(985, 520)
self.retranslateUi(Dialog)
QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
Dialog.setWindowTitle(QApplication.translate("Dialog", "Dicom Tree", None, QApplication.UnicodeUTF8))
[docs]class DicomTree(object):
"""
Dicom tree viewer window for uploading dicom file and display all it's attribute in tree structure
"""
def __init__(self, parent):
self.parent = parent
[docs] def showTree(self, filepath, dialog, layout, view, model, patientBrowser, planBrowser):
"""
Show data in tree
"""
# populate data
dicomtree = dcm_qt_tree.DicomTree(filepath, model)
# Dicom Tree Data
model = dicomtree.show_tree()
# Patient Info Table
patient_table = dicomtree.get_patient_table()
patientBrowser.append(patient_table)
patientBrowser.show()
# Plan Info Table
plan_table = dicomtree.get_plan_table()
planBrowser.append(plan_table)
planBrowser.show()
view.setModel(model)
view.show()
# store pixel data
pixel_array = dicomtree.pixel_array
if pixel_array is not None:
self.displayImage(dialog, layout, pixel_array)
return view
def displayImage(self, dialog, layout, pixel_array):
"""
Display dicom 2D image
"""
if pixel_array is not None:
from matplotlib import pyplot as plt
image_path = 'dicom.png' #path to your image file
plt.imshow(pixel_array, cmap=plt.gray())
plt.savefig(image_path)
new_frame = QWidget()
new_frame.setGeometry(QRect(30, 540, 921, 500))
label_Image = QLabel(new_frame)
image_profile = QImage(image_path) #QImage object
image_profile = image_profile.scaled(650,487, aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation) # To scale image for example and keep its Aspect Ration
label_Image.setPixmap(QPixmap.fromImage(image_profile))
layout.addWidget(new_frame) # the matplotlib canvas
dialog.resize(985, 1000)
def openFile(self, dialog):
qfDialog = QFileDialog()
filename = qfDialog.getOpenFileName(dialog, 'Open File', '.')
filepath = str(filename[0])
return filepath
def callTree(self, filepath, dialog, layout, view, model, patientBrowser, planBrowser):
# Clear all values before uploading the file
model.clear()
model.setHorizontalHeaderLabels(['Name', 'Value'])
patientBrowser.clear()
planBrowser.clear()
dialog.resize(985, 520)
for i in reversed(range(layout.count())):
layout.itemAt(i).widget().setParent(None)
# Set Filename as a label in Dialog box
title = filepath.rsplit("/")[-1]
self.btnlabel.setText(QApplication.translate("Dialog", title, None, QApplication.UnicodeUTF8))
# Call Show Tree method
self.showTree(filepath, dialog, layout, view, model, patientBrowser, planBrowser)
def triggerEvent(self, dialog, layout, view, model, patientBrowser, planBrowser):
filepath = self.openFile(dialog)
self.callTree(filepath, dialog, layout, view, model, patientBrowser, planBrowser)
[docs] def dicomtree(self):
"""
Show dicom tree window
"""
dicomDialog = QDialog(self.parent)
dicomUi = Dicom_Dialog()
dicomUi.setupUi(dicomDialog)
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(dicomDialog.sizePolicy().hasHeightForWidth())
dicomDialog.setSizePolicy(sizePolicy)
qbtn = QPushButton('Upload DICOM File', dicomDialog)
qbtn.resize(921, 51)
qbtn.move(30, 20)
self.btnlabel = QLabel(dicomDialog)
self.btnlabel.setGeometry(QRect(30, 80, 921, 20))
self.btnlabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.btnlabel.setObjectName("Upload file title")
self.label1 = QLabel(dicomDialog)
self.label1.setGeometry(QRect(30, 330, 100, 31))
self.label1.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.label1.setObjectName("Patient")
self.label1.setText(QApplication.translate("Dialog", "Patient Info:", None, QApplication.UnicodeUTF8))
self.label2 = QLabel(dicomDialog)
self.label2.setGeometry(QRect(500, 330, 100, 31))
self.label2.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.label2.setObjectName("Plan")
self.label2.setText(QApplication.translate("Dialog", "Plan Info:", None, QApplication.UnicodeUTF8))
dicomDialog.setStyleSheet("QLabel { font-weight:bold; font-size: 15px; }")
# init widgets
view = QTreeView(dicomDialog)
view.setSelectionBehavior(QAbstractItemView.SelectRows)
model = QStandardItemModel()
model.setHorizontalHeaderLabels(['Name', 'Value'])
view.setModel(model)
view.setUniformRowHeights(False)
view.header().setResizeMode(QHeaderView.ResizeToContents)
view.resize(921, 220)
view.move(30, 110)
view.show()
patientBrowser = QTextBrowser(dicomDialog)
patientBrowser.setGeometry(QRect(30, 360, 450, 80))
patientBrowser.show()
planBrowser = QTextBrowser(dicomDialog)
planBrowser.setGeometry(QRect(500, 360, 450, 80))
planBrowser.show()
frame = QFrame(dicomDialog)
frame.setGeometry(QRect(30, 450, 921, 650))
layout = QVBoxLayout(frame)
frame.show()
qbtn.clicked.connect(lambda: self.triggerEvent(dicomDialog, layout, view, model, patientBrowser, planBrowser))
dicomDialog.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
form = LicenseAgreement()
form.show()
sys.exit(app.exec_())