Skip to content

Commit fe163bf

Browse files
authored
Merge pull request #3647 from jsiirola/viewer-pyqt6
Update Viewer for PyQt6, improve load time
2 parents 828560c + 97cbccf commit fe163bf

File tree

5 files changed

+174
-109
lines changed

5 files changed

+174
-109
lines changed

pyomo/contrib/viewer/model_browser.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,12 @@ def __init__(self, ui_data, standard="Var"):
168168
self.treeView.setModel(datmodel)
169169
self.treeView.setColumnWidth(0, 400)
170170
# Selection behavior: select a whole row, can select multiple rows.
171-
self.treeView.setSelectionBehavior(myqt.QAbstractItemView.SelectRows)
172-
self.treeView.setSelectionMode(myqt.QAbstractItemView.ExtendedSelection)
171+
self.treeView.setSelectionBehavior(
172+
myqt.QAbstractItemView.SelectionBehavior.SelectRows
173+
)
174+
self.treeView.setSelectionMode(
175+
myqt.QAbstractItemView.SelectionMode.ExtendedSelection
176+
)
173177

174178
def refresh(self):
175179
added = self.datmodel._update_tree()
@@ -595,9 +599,9 @@ def data(
595599
if isinstance(
596600
index.internalPointer().data, (Block, Block._ComponentDataClass)
597601
):
598-
return myqt.QColor(myqt.QtCore.Qt.black)
602+
return myqt.QColor(myqt.QtCore.Qt.GlobalColor.black)
599603
else:
600-
return myqt.QColor(myqt.QtCore.Qt.blue)
604+
return myqt.QColor(myqt.QtCore.Qt.GlobalColor.blue)
601605
else:
602606
return
603607

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2025
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# ___________________________________________________________________________
10+
#
11+
# This module was originally developed as part of the IDAES PSE Framework
12+
#
13+
# Institute for the Design of Advanced Energy Systems Process Systems
14+
# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, by the
15+
# software owners: The Regents of the University of California, through
16+
# Lawrence Berkeley National Laboratory, National Technology & Engineering
17+
# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia
18+
# University Research Corporation, et al. All rights reserved.
19+
#
20+
# This software is distributed under the 3-clause BSD License.
21+
# ___________________________________________________________________________
22+
23+
import pyomo.contrib.viewer.qt as myqt
24+
from pyomo.contrib.viewer.pyomo_viewer import qtconsole_app, qtconsole_available
25+
26+
27+
class QtApp(qtconsole_app.JupyterQtConsoleApp if qtconsole_available else object):
28+
_kernel_cmd_show_ui = """try:
29+
ui.show()
30+
except NameError:
31+
try:
32+
model
33+
except NameError:
34+
model=None
35+
ui = get_mainwindow(model=model, ask_close=False)
36+
ui.setWindowTitle('Pyomo Model Viewer -- {}')"""
37+
38+
_kernel_cmd_hide_ui = """try:
39+
ui.hide()
40+
except NameError:
41+
pass"""
42+
43+
_kernel_cmd_import_qt_magic = r"%gui qt"
44+
45+
_kernel_cmd_import_ui = "from pyomo.contrib.viewer.ui import get_mainwindow"
46+
47+
_kernel_cmd_import_pyomo_env = "import pyomo.environ as pyo"
48+
49+
def active_widget_name(self):
50+
current_widget = self.window.tab_widget.currentWidget()
51+
current_widget_index = self.window.tab_widget.indexOf(current_widget)
52+
return self.window.tab_widget.tabText(current_widget_index).replace("'", '"')
53+
54+
def show_ui(self):
55+
kc = self.window.active_frontend.kernel_client
56+
kc.execute(
57+
self._kernel_cmd_show_ui.format(self.active_widget_name()), silent=True
58+
)
59+
60+
def hide_ui(self):
61+
kc = self.window.active_frontend.kernel_client
62+
kc.execute(self._kernel_cmd_hide_ui, silent=True)
63+
64+
def run_script(self, checked=False, filename=None):
65+
"""Run a python script in the current kernel."""
66+
if filename is None:
67+
# Show a dialog box for user to select working directory
68+
filename = myqt.QtWidgets.QFileDialog.getOpenFileName(
69+
self.window,
70+
"Run Script",
71+
os.getcwd(),
72+
"py (*.py);;text (*.txt);;all (*)",
73+
)
74+
if filename[0]: # returns a tuple of file and filter or ("","")
75+
filename = filename[0]
76+
else:
77+
filename = None
78+
# Run script if one was selected
79+
if filename is not None:
80+
kc = self.window.active_frontend.kernel_client
81+
kc.execute("%run {}".format(filename))
82+
83+
def kernel_pyomo_init(self, kc):
84+
kc.execute(self._kernel_cmd_import_qt_magic, silent=True)
85+
kc.execute(self._kernel_cmd_import_ui, silent=True)
86+
kc.execute(self._kernel_cmd_import_pyomo_env, silent=False)
87+
88+
def init_qt_elements(self):
89+
super().init_qt_elements()
90+
self.kernel_pyomo_init(self.widget.kernel_client)
91+
self.run_script_act = myqt.QAction("&Run Script...", self.window)
92+
self.show_ui_act = myqt.QAction("&Show Pyomo Model Viewer", self.window)
93+
self.hide_ui_act = myqt.QAction("&Hide Pyomo Model Viewer", self.window)
94+
self.window.file_menu.addSeparator()
95+
self.window.file_menu.addAction(self.run_script_act)
96+
self.window.view_menu.addSeparator()
97+
self.window.view_menu.addAction(self.show_ui_act)
98+
self.window.view_menu.addAction(self.hide_ui_act)
99+
self.window.view_menu.addSeparator()
100+
self.run_script_act.triggered.connect(self.run_script)
101+
self.show_ui_act.triggered.connect(self.show_ui)
102+
self.hide_ui_act.triggered.connect(self.hide_ui)
103+
104+
def new_frontend_master(self):
105+
widget = super().new_frontend_master()
106+
self.kernel_pyomo_init(widget.kernel_client)
107+
return widget

pyomo/contrib/viewer/pyomo_viewer.py

Lines changed: 21 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -20,109 +20,36 @@
2020
# This software is distributed under the 3-clause BSD License.
2121
# ___________________________________________________________________________
2222

23-
import os
24-
25-
from pyomo.common.dependencies import attempt_import, UnavailableClass
23+
from pyomo.common.dependencies import attempt_import
24+
from pyomo.common.deprecation import relocated_module_attribute
2625
from pyomo.scripting.pyomo_parser import add_subparser
27-
import pyomo.contrib.viewer.qt as myqt
28-
29-
qtconsole_app, qtconsole_available = attempt_import("qtconsole.qtconsoleapp")
30-
31-
32-
class QtApp(
33-
qtconsole_app.JupyterQtConsoleApp
34-
if qtconsole_available
35-
else UnavailableClass(qtconsole_app)
36-
):
37-
_kernel_cmd_show_ui = """try:
38-
ui.show()
39-
except NameError:
40-
try:
41-
model
42-
except NameError:
43-
model=None
44-
ui = get_mainwindow(model=model, ask_close=False)
45-
ui.setWindowTitle('Pyomo Model Viewer -- {}')"""
46-
47-
_kernel_cmd_hide_ui = """try:
48-
ui.hide()
49-
except NameError:
50-
pass"""
51-
52-
_kernel_cmd_import_qt_magic = r"%gui qt"
53-
54-
_kernel_cmd_import_ui = "from pyomo.contrib.viewer.ui import get_mainwindow"
55-
56-
_kernel_cmd_import_pyomo_env = "import pyomo.environ as pyo"
57-
58-
def active_widget_name(self):
59-
current_widget = self.window.tab_widget.currentWidget()
60-
current_widget_index = self.window.tab_widget.indexOf(current_widget)
61-
return self.window.tab_widget.tabText(current_widget_index)
62-
63-
def show_ui(self):
64-
kc = self.window.active_frontend.kernel_client
65-
kc.execute(
66-
self._kernel_cmd_show_ui.format(self.active_widget_name()), silent=True
67-
)
6826

69-
def hide_ui(self):
70-
kc = self.window.active_frontend.kernel_client
71-
kc.execute(self._kernel_cmd_hide_ui, silent=True)
72-
73-
def run_script(self, checked=False, filename=None):
74-
"""Run a python script in the current kernel."""
75-
if filename is None:
76-
# Show a dialog box for user to select working directory
77-
filename = myqt.QtWidgets.QFileDialog.getOpenFileName(
78-
self.window,
79-
"Run Script",
80-
os.getcwd(),
81-
"py (*.py);;text (*.txt);;all (*)",
82-
)
83-
if filename[0]: # returns a tuple of file and filter or ("","")
84-
filename = filename[0]
85-
else:
86-
filename = None
87-
# Run script if one was selected
88-
if filename is not None:
89-
kc = self.window.active_frontend.kernel_client
90-
kc.execute("%run {}".format(filename))
91-
92-
def kernel_pyomo_init(self, kc):
93-
kc.execute(self._kernel_cmd_import_qt_magic, silent=True)
94-
kc.execute(self._kernel_cmd_import_ui, silent=True)
95-
kc.execute(self._kernel_cmd_import_pyomo_env, silent=False)
96-
97-
def init_qt_elements(self):
98-
super().init_qt_elements()
99-
self.kernel_pyomo_init(self.widget.kernel_client)
100-
self.run_script_act = myqt.QAction("&Run Script...", self.window)
101-
self.show_ui_act = myqt.QAction("&Show Pyomo Model Viewer", self.window)
102-
self.hide_ui_act = myqt.QAction("&Hide Pyomo Model Viewer", self.window)
103-
self.window.file_menu.addSeparator()
104-
self.window.file_menu.addAction(self.run_script_act)
105-
self.window.view_menu.addSeparator()
106-
self.window.view_menu.addAction(self.show_ui_act)
107-
self.window.view_menu.addAction(self.hide_ui_act)
108-
self.window.view_menu.addSeparator()
109-
self.run_script_act.triggered.connect(self.run_script)
110-
self.show_ui_act.triggered.connect(self.show_ui)
111-
self.hide_ui_act.triggered.connect(self.hide_ui)
27+
relocated_module_attribute(
28+
'QtApp', 'pyomo.contrib.viewer.pyomo_qtapp.QtApp', version='6.9.3.dev0'
29+
)
11230

113-
def new_frontend_master(self):
114-
widget = super().new_frontend_master()
115-
self.kernel_pyomo_init(widget.kernel_client)
116-
return widget
31+
qtconsole_app, qtconsole_available = attempt_import(
32+
"qtconsole.qtconsoleapp", defer_import=False
33+
)
11734

11835

11936
def main(*args):
37+
# Import the Qt infrastructure (if it exists)
38+
import pyomo.contrib.viewer.qt as myqt
39+
12040
if not myqt.available or not qtconsole_available:
12141
errors = list(myqt.import_errors)
12242
if not qtconsole_available:
12343
errors.append(qtconsole_app._moduleunavailable_message())
12444
print("qt not available\n " + "\n ".join(errors))
12545
return
46+
47+
# Ensure that all Pyomo plugins have been registered
48+
import pyomo.environ
49+
50+
# Import & run the Qt application
51+
from pyomo.contrib.viewer.pyomo_qtapp import QtApp
52+
12653
QtApp.launch_instance()
12754

12855

@@ -134,3 +61,6 @@ def main(*args):
13461
add_help=False,
13562
description="This runs the Pyomo model viewer",
13663
)
64+
65+
if __name__ == '__main__':
66+
main()

pyomo/contrib/viewer/qt.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,27 @@
2626
use some dummy classes to allow some testing.
2727
"""
2828
__author__ = "John Eslick"
29-
29+
import sys
3030
import enum
3131
import importlib
3232

33+
from pyomo.common.collections import Bunch
3334
from pyomo.common.flags import building_documentation
3435

3536
# Supported Qt wrappers in preferred order
36-
supported = ["PySide6", "PyQt5"]
37+
supported = ["PySide6", "PyQt6", "PyQt5"]
3738
# Import errors encountered, delay logging for testing reasons
3839
import_errors = []
3940
# Set this to the Qt wrapper module is available
4041
available = False
4142

43+
# You can only have one Qt interface loaded at a time. If something
44+
# like IPython has already loaded an interface, then we will use that
45+
# one.
46+
_loaded = list(filter(sys.modules.__contains__, supported))
47+
if _loaded:
48+
supported = _loaded
49+
4250
for module_str in supported:
4351
try:
4452
qt_package = importlib.import_module(module_str)
@@ -48,6 +56,7 @@
4856
available = module_str
4957
break
5058
except Exception as e:
59+
qt_package = QtWidgets = QtCore = QtGui = None
5160
import_errors.append(f"{e}")
5261

5362
if not available:
@@ -104,7 +113,6 @@ class QItemDelegate(object):
104113
QAbstractItemView = QtWidgets.QAbstractItemView
105114
QFileDialog = QtWidgets.QFileDialog
106115
QMainWindow = QtWidgets.QMainWindow
107-
QMainWindow = QtWidgets.QMainWindow
108116
QMdiArea = QtWidgets.QMdiArea
109117
QApplication = QtWidgets.QApplication
110118
QTableWidgetItem = QtWidgets.QTableWidgetItem
@@ -119,17 +127,31 @@ class QItemDelegate(object):
119127
QColor = QtGui.QColor
120128
QAbstractItemModel = QtCore.QAbstractItemModel
121129
QAbstractTableModel = QtCore.QAbstractTableModel
122-
QMetaType = QtCore.QMetaType
123130
Qt = QtCore.Qt
131+
QMetaType = Bunch()
124132
if available == "PySide6":
125133
from PySide6.QtGui import QAction
126134
from PySide6.QtCore import Signal
127135
from PySide6 import QtUiTools as uic
136+
137+
QMetaType.Int = QtCore.QMetaType.Type.Int.value
138+
QMetaType.Double = QtCore.QMetaType.Type.Double.value
139+
elif available == "PyQt6":
140+
from PyQt6.QtGui import QAction
141+
from PyQt6.QtCore import pyqtSignal as Signal
142+
from PyQt6 import uic
143+
144+
QMetaType.Int = QtCore.QMetaType.Type.Int.value
145+
QMetaType.Double = QtCore.QMetaType.Type.Double.value
128146
elif available == "PyQt5":
129147
from PyQt5.QtWidgets import QAction
130148
from PyQt5.QtCore import pyqtSignal as Signal
131149
from PyQt5 import uic
132150

151+
QMetaType.Int = QtCore.QMetaType.Type.Int
152+
QMetaType.Double = QtCore.QMetaType.Type.Double
153+
else:
154+
raise RuntimeError(f"Unknown Qt engine: {available}")
133155
# Note that QAbstractTableModel and QAbstractItemModel have
134156
# signatures that are not parsable by Sphinx, so we will hide them
135157
# if we are building the API documentation.

0 commit comments

Comments
 (0)