Пока не забылось, изложу последовательно всю эпопею и подведу некоторые итоги.
Пусть будет тут в назидание мне и интересующимся. Уж больно забавный результат
получился .
Если по порядку, то всё началось с пожелания, чтобы главное окно приложения
открывалось именно на том экране многоэкранной системы, с которого его запустили.
Под многоэкранной системой здесь понимается компьютер, к видеосистеме которого
подключены несколько мониторов, один из которых — основной, а остальные —
дополнительные. В общем случае, эти мониторы могут иметь разные размеры
и разрешение экрана.
Старт приложения может выполняться как щелчком ЛКМ по значку запуска приложения,
расположенном на экране, так и из командной строки в окне терминала, открытого
на этом экране. Приложение должно определить экран, с которого его запустили,
открыть главное окно приложения размером в половину высоты и ширины этого экрана
и разместить его по центру данного экрана.
Ну и до кучи, приложение должно быть кроссплатформенным и работать одинаково
как под Windows, так и под Linux.
И что тут сложного? Задачка-то тривиальная. Пишем пробник:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QMainWindow
if __name__ == '__main__':
app = QApplication(sys.argv)
mwin = QMainWindow()
mwin.show()
sys.exit(app.exec_()) |
|
Раскидываем значки по экранам, проверяем, убеждаемся, что по щелчкам ЛКМ на значках
приложение сразу открывает своё окно на нужном экране и под Linux, и под Windows.
Убеждаемся, что и для командной строки результат аналогичен. Чего тут думать-то?
Осталось-то только поменять размер окна под выбранный экран, отцентровать его и...
эпично лажануться .
Это только кажется, что решение уже найдено. На самом деле, полного решения
не существует в рамках ограничений, накладываемых постановкой задачи.
В качестве примера приведу некий код, на котором можно проверить все утверждения,
приводимые ниже (убрал под спойлер для истории).
Кликните здесь для просмотра всего текста
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
"""QScreen Probe v0.003
Пример реализации оконного приложения с изменением геометрии окна
в многоэкранных системах, когда приложение должно открывать
рабочее окно именно на том экране, с которого его запустили.
"""
import sys, os, platform, time
from PyQt5 import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QAction, QApplication
from PyQt5.QtWidgets import QLabel, QMainWindow, QMessageBox
APP_NAME = 'QScreen Probe v0.003'
def probe_get_screen(client):
"""Определяем экран, выбранный системой для отрисовки окна:
selected_screen = probe_get_screen(client)
"""
selected_screen = None
if (int(QT_VERSION_STR.split('.')[1]) >= 10 and
int(PYQT_VERSION_STR.split('.')[1]) >= 10):
selected_screen = QApplication.screenAt(client.geometry().bottomLeft())
else:
screens = QApplication.screens()
if len(screens) > 0:
for i in range(len(screens)):
if screens[i].geometry().contains(client.geometry().bottomLeft()):
selected_screen = screens[i]
return selected_screen
class ProbeMainWindow(QMainWindow):
"""Главное окно приложения
"""
is_first_show = True
def __init__(self, parent=None):
super(ProbeMainWindow, self).__init__(parent)
self.initUI()
def initUI(self):
self.setWindowTitle(APP_NAME)
self.statusBar()
orphanAction = QAction(
'Без родителя', self, shortcut='Ctrl+1',
statusTip='Окно сообщения, у которого нет родителя',
triggered=self.to_orphan, enabled=True)
infantAction = QAction(
'С родителем', self, shortcut='Ctrl+2',
statusTip='Окно сообщения, у которого есть родитель',
triggered=self.to_infant, enabled=True)
exitAction = QAction(
'Выход', self, shortcut='Ctrl+Q',
statusTip='Завершить работу приложения',
triggered=self.to_close, enabled=True)
taskMenu = self.menuBar().addMenu('Файл')
taskMenu.addAction(orphanAction)
taskMenu.addAction(infantAction)
taskMenu.addAction(exitAction)
infoText = QLabel('Главное окно приложения открывается:\n\n'
'под Windows - на том экране, на котором в момент открытия\n'
'окна располагается указатель мыши,\n\n'
'под Linux - на том экране, с которого запустили приложение.')
self.setCentralWidget(infoText)
# Поскольку графический интерфейс надо построить с учётом того,
# что приложение может быть запущено в многоэкранных системах,
# то предварительно требуется узнать какой экран использует
# система для размещения окна приложения.
# Поэтому просто ждём сработки self.showEvent()
# после выполнения mwin.show() в __main__.
def showEvent(self, event):
if self.isVisible() and self.is_first_show:
# Ловим первое событие отрисовки окна и отправляем окно
# на изменение геометрии.
# Поскольку в Linux требуется некоторое время, чтобы X11
# успел отрисовать окно, а Windows готова сразу,
# то, чтобы не множить сущности, просто отправляем сигнал
# на изменение геометрии с соответствущей задержкой.
self.is_first_show = False
custom_delay = 150
if sys.platform == 'win32':
custom_delay = 0
QTimer.singleShot(custom_delay, self.to_custom)
super(ProbeMainWindow, self).showEvent(event)
@pyqtSlot()
def to_custom(self):
"""Меняем геометрию окна
"""
selected_screen = probe_get_screen(self)
if selected_screen is None:
# нет устройств отображения
print('ОШИБКА! Текущий экран не определён.')
self.to_close()
else:
current_geo = selected_screen.availableGeometry()
self.setGeometry(
current_geo.x() + current_geo.width() // 4,
current_geo.y() + current_geo.height() // 4,
current_geo.width() // 2,
current_geo.height() // 2)
def to_orphan(self):
# моделируем затяжное формирование окна
# time.sleep(3)
x = QMessageBox.warning(
None, 'БЕЗ РОДИТЕЛЯ',
'Окно этого сообщения открывается на том экране,'
' где в данный момент располагается указатель мыши.')
def to_infant(self):
# моделируем затяжное формирование окна
# time.sleep(3)
x = QMessageBox.information(
self, 'С РОДИТЕЛЕМ',
'Окно этого сообщения открывается на том же экране,'
' где находится родитель (главное окно).')
def to_close(self):
"""Завершаем работу.
"""
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
# моделируем затяжной пуск приложения
# time.sleep(3)
# если есть хоть один параметр, то пуск - аварийный
if len(sys.argv) > 1:
x = QMessageBox.critical(
None, 'АВАРИЯ',
'Окно этого сообщения открывается на том экране,'
' где в данный момент располагается указатель мыши.\n\n'
'Работа приложения {!s} будет завершена.'.format(APP_NAME))
sys.exit(1)
# нормальный запуск приложения
mwin = ProbeMainWindow()
mwin.show()
sys.exit(app.exec_()) |
|
Сначала о хорошем — Linux может. Но только за счёт особенностей X11, частично
разбираемых в последнем разделе тут https://doc.qt.io/qt-5/application-windows.html
и, видимо, нарушающих в какой-то степени принятые соглашения о пользовательском
интерфейсе для окон верхнего уровня.
Здесь сигнал на изменение геометрии окна приходится посылать из обработчика
события showEvent с некоторой задержкой, но экран, с которого запустили приложение,
в результате определяется правильно. По крайней мере, на Lubutu, Xubuntu и OpenSUSE
работает, а покрыть всю линейку одному — здоровья не хватит . Недостатки видны
сразу — мелькание артефактного окна. Но это следствие того, что окно должно быть
уже прорисовано до изменения геометрии.
Windows задержки не требует, но в качестве экрана для запуска приложения всегда
выбирает тот, на котором в момент открытия окна находится указатель мыши (sic!).
Надо сказать, что поначалу это обескураживает даже матёрых пользователей Windows,
которые раньше просто не работали с многоэкранными системами. Например, ситуация,
когда приложение запускается достаточно долго, а пользователь, стартовавший
приложение, успевает переместить указатель мыши на другой экран до открытия окна
приложения, поначалу не вызывает понимания.
Ничего необычного в таком выборе экрана нет. Похоже, такое поведение полностью
соответствует принятым соглашениям о пользовательском интерфейсе для окон верхнего
уровня. Например, даже в Linux окна верхнего уровня, создаваемые функциями типа:
| Python | 1
| result = QMessageBox.critical(None, 'Авария !!!') |
|
открываются именно на том экране, на котором находится указатель мыши. Видимо,
в этом случае приняты необходимые и достаточные меры, чтобы соответствовать
стандарту.
Если постановку задачи не изменить, то выходом из ситуации видится только
дрессировка пользователей Windows, которых следует приучить удерживать
указатель мыши до открытия окна приложения на том экране, с которого
оно запущено.
Это дело неблагодарное. Дешевле изменить постановку и сделать явное указание экрана
через параметры запуска или придумать что-нибудь с конфигурированием.
Дополнение от 17.02.2022:
Однако, надо забить последний гвоздь в эту тему и больше уже к ней не возвращаться.
Для начала надо внести поправочку в формулировки, потому как под Linux начальное (главное)
окно открывается всё-таки не на том экране, с которого запустили приложение, а на экране,
который был активным во время открытия окна:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
"""QScreen Probe v0.004
Пример реализации оконного приложения с изменением геометрии окна
в многоэкранных системах, когда приложение должно открывать
главное окно на активном экране.
"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QAction, QHBoxLayout, QWidget
from PyQt5.QtWidgets import QLabel, QMainWindow, QMessageBox
APP_NAME = 'QScreen Probe v0.004'
def probe_get_screen(client):
"""Определяем экран, выбранный системой для отрисовки окна:
selected_screen = probe_get_screen(client)
"""
selected_screen = QApplication.screens()[0]
if (int(QT_VERSION_STR.split('.')[1]) >= 10 and
int(PYQT_VERSION_STR.split('.')[1]) >= 10):
selected_screen = QApplication.screenAt(client.geometry().bottomLeft())
else:
screens = QApplication.screens()
if len(screens) > 0:
for i in range(len(screens)):
if screens[i].geometry().contains(client.geometry().bottomLeft()):
selected_screen = screens[i]
return selected_screen
class ProbeMainWindow(QMainWindow):
"""Главное окно приложения
"""
def __init__(self, parent=None):
super(ProbeMainWindow, self).__init__(parent)
self.is_first_show = True
# Поскольку графический интерфейс надо построить с учётом того,
# что приложение может быть запущено в многоэкранных системах,
# то предварительно требуется узнать какой экран использует
# система для размещения окна приложения.
# Поэтому просто ждём сработки self.showEvent()
# после выполнения mwin.show() в __main__.
def showEvent(self, event):
if self.isVisible() and self.is_first_show:
# X11 в Linux запаздывает с отрисовкой первого окна
# верхнего уровня, поэтому ловим первое событие
# отрисовки и отправляем окно на изменение размеров
# и расположения с соответствующей задержкой.
self.is_first_show = False
QTimer.singleShot(80 if sys.platform == 'linux' else 10,
self.to_custom)
super(ProbeMainWindow, self).showEvent(event)
@pyqtSlot()
def to_custom(self):
"""Центруем, меняем размеры окна под выбранный системой экран
и отрисовываем графический интерфейс
"""
selected_screen = probe_get_screen(self)
current_geo = selected_screen.availableGeometry()
self.move(current_geo.x() + current_geo.width() // 4,
current_geo.y() + current_geo.height() // 4)
self.resize(current_geo.width() // 2,
current_geo.height() // 2)
QApplication.processEvents()
self.setWindowTitle(APP_NAME)
self.statusBar()
QApplication.processEvents()
orphanAction = QAction(
'Без родителя', self, shortcut='Ctrl+1',
statusTip='Окно сообщения, у которого нет родителя',
triggered=self.to_orphan, enabled=True)
infantAction = QAction(
'С родителем', self, shortcut='Ctrl+2',
statusTip='Окно сообщения, у которого есть родитель',
triggered=self.to_infant, enabled=True)
exitAction = QAction(
'Выход', self, shortcut='Ctrl+Q',
statusTip='Завершить работу приложения',
triggered=self.to_close, enabled=True)
taskMenu = self.menuBar().addMenu('Файл')
taskMenu.addAction(orphanAction)
taskMenu.addAction(infantAction)
taskMenu.addAction(exitAction)
infoText = QLabel(
'При запуске на многоэкранных системах главное окно\n'
'этого приложения открывается:\n\n'
'под Windows - на том экране, на котором в момент открытия\n'
'окна находится указатель мыши,\n\n'
'под Linux - на том экране, который был активен в момент\n'
'открытия окна.\n\n\n\n'
'Ctrl-1 - сообщение без родителя откроется на том экране,\n'
'где находится указатель мыши.\n\n'
'Ctrl-2 - сообщение с родителем откроется на том экране,\n'
'где находится родитель (главное окно).'
)
font = infoText.font()
font.setWeight(QFont.Bold)
infoText.setFont(font)
main_map = QHBoxLayout(None)
main_map.addStretch(1)
main_map.addWidget(infoText)
main_map.addStretch(1)
central_widget = QWidget(self)
central_widget.setLayout(main_map)
self.setCentralWidget(central_widget)
def to_orphan(self):
QMessageBox.warning(
None, 'БЕЗ РОДИТЕЛЯ',
'Окно этого сообщения открывается на том экране,'
' где в данный момент располагается указатель мыши.')
def to_infant(self):
QMessageBox.information(
self, 'С РОДИТЕЛЕМ',
'Окно этого сообщения открывается на том же экране,'
' где находится родитель (главное окно).')
def to_close(self):
"""Завершаем работу.
"""
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
# если есть хоть один параметр, то пуск - аварийный
if len(sys.argv) > 1:
QMessageBox.critical(
None, 'АВАРИЯ',
'Окно этого сообщения открывается на том экране,'
' где в данный момент располагается указатель мыши.\n\n'
'Работа приложения {!s} будет завершена.'.format(APP_NAME))
sys.exit(1)
# нормальный запуск приложения
mwin = ProbeMainWindow()
import time
time.sleep(3)
mwin.show()
sys.exit(app.exec_()) |
|
Выглядит это так. Например, запускаем приложение с задержкой исполнения из сессии командной
строки, открытой на дополнительном экране:
| Code | 1
| sleep 3 && python3 screenprobe.py |
|
Если затем просто переместить указатель мыши на основной экран, то приложение откроется
на дополнительном экране. А если при этом щёлкнуть ЛКМ, активировав основной экран,
то приложение откроется уже на основном. Под Windows же, и в том, и в другом случае,
приложение открывается на основном экране (щёлкать ЛКМ не надо, главное - указатель мыши перевести).
А поскольку именно такого поведения требуют принятые соглашения о пользовательском
интерфейсе для окон верхнего уровня (которые Linux нарушает, как это было сказано выше),
то проще, как оказалось, добавить соответствующий код в приложение, чтобы его поведение
было одинаковым и тут, и там:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
"""QScreen Probe v0.005
Пример реализации оконного приложения с изменением геометрии окна
в многоэкранных системах, когда приложение должно открывать
рабочее окно на том экране, на котором в момент открытия находится
указатель мыши.
"""
import sys
from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor, QFont
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QAction, QHBoxLayout, QWidget
from PyQt5.QtWidgets import QLabel, QMainWindow, QMessageBox
APP_NAME = 'QScreen Probe v0.005'
def probe_cursor_screen(client):
"""Определяем экран, на котором находится указатель мыши:
selected_screen = probe_cursor_screen(client)
"""
selected_screen = QApplication.screens()[0]
if (int(QT_VERSION_STR.split('.')[1]) >= 10 and
int(PYQT_VERSION_STR.split('.')[1]) >= 10):
selected_screen = QApplication.screenAt(QCursor.pos())
else:
screens = QApplication.screens()
if len(screens) > 0:
for i in range(len(screens)):
if screens[i].geometry().contains(QCursor.pos()):
selected_screen = screens[i]
return selected_screen
class ProbeMainWindow(QMainWindow):
"""Главное окно приложения
"""
def __init__(self, parent=None):
super(ProbeMainWindow, self).__init__(parent)
self.is_first_show = True
self.initUI()
def initUI(self):
self.setWindowTitle(APP_NAME)
self.statusBar()
orphanAction = QAction(
'Без родителя', self, shortcut='Ctrl+1',
statusTip='Окно сообщения, у которого нет родителя',
triggered=self.to_orphan, enabled=True)
infantAction = QAction(
'С родителем', self, shortcut='Ctrl+2',
statusTip='Окно сообщения, у которого есть родитель',
triggered=self.to_infant, enabled=True)
exitAction = QAction(
'Выход', self, shortcut='Ctrl+Q',
statusTip='Завершить работу приложения',
triggered=self.to_close, enabled=True)
taskMenu = self.menuBar().addMenu('Файл')
taskMenu.addAction(orphanAction)
taskMenu.addAction(infantAction)
taskMenu.addAction(exitAction)
infoText = QLabel(
'При запуске на многоэкранных системах главное окно\n'
'этого приложения и под Windows, и под Linux открывается\n'
'на том экране, на котором в момент открытия окна\n'
'находится указатель мыши.\n\n\n\n'
'Ctrl-1 - сообщение без родителя откроется на том экране,\n'
'где находится указатель мыши.\n\n'
'Ctrl-2 - сообщение с родителем откроется на том экране,\n'
'где находится родитель (главное окно).'
)
font = infoText.font()
font.setWeight(QFont.Bold)
infoText.setFont(font)
main_map = QHBoxLayout(None)
main_map.addStretch(1)
main_map.addWidget(infoText)
main_map.addStretch(1)
central_widget = QWidget(self)
central_widget.setLayout(main_map)
self.setCentralWidget(central_widget)
# Центруем и меняем размеры окна под экран, на котором находится
# указатель мыши (актуально для Linux и Windows не портит)
selected_screen = probe_cursor_screen(self)
current_geo = selected_screen.availableGeometry()
self.move(current_geo.x() + current_geo.width() // 4,
current_geo.y() + current_geo.height() // 4)
self.resize(current_geo.width() // 2,
current_geo.height() // 2)
def to_orphan(self):
QMessageBox.warning(
None, 'БЕЗ РОДИТЕЛЯ',
'Окно этого сообщения открывается на том экране,'
' где в данный момент располагается указатель мыши.')
def to_infant(self):
QMessageBox.information(
self, 'С РОДИТЕЛЕМ',
'Окно этого сообщения открывается на том же экране,'
' где находится родитель (главное окно).')
def to_close(self):
"""Завершаем работу.
"""
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
# если есть хоть один параметр, то пуск - аварийный
if len(sys.argv) > 1:
QMessageBox.critical(
None, 'АВАРИЯ',
'Окно этого сообщения открывается на том экране,'
' где в данный момент располагается указатель мыши.\n\n'
'Работа приложения {!s} будет завершена.'.format(APP_NAME))
sys.exit(1)
# нормальный запуск приложения
mwin = ProbeMainWindow()
mwin.show()
sys.exit(app.exec_()) |
|
История вопроса здесь:
Как определить во время инициализации на какой экран отрисуется окно?
https://www.cyberforum.ru/blog... g6887.html
https://www.cyberforum.ru/blog... g6917.html
|