- Steam networking dependencies - Full-size regions map - Economy base structures and assets
604 lines
22 KiB
Python
604 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Region Mapper — большая цветовая карта -> JSON {region: "#RRGGBB"}.
|
||
Зависимости: pip install PyQt5 Pillow
|
||
Запуск: python nudge.py map.bmp
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import json
|
||
import math
|
||
|
||
from PyQt5.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QTimer, pyqtSignal
|
||
from PyQt5.QtGui import QImage, QPixmap, QPainter, QColor, QPen, QKeySequence
|
||
from PyQt5.QtWidgets import (
|
||
QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
||
QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem,
|
||
QSplitter, QFileDialog, QMessageBox, QInputDialog, QShortcut,
|
||
)
|
||
from PIL import Image
|
||
|
||
Image.MAX_IMAGE_PIXELS = None
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# Утилиты
|
||
# ----------------------------------------------------------------------
|
||
|
||
def rgb_to_hex(rgb):
|
||
return "#{:02X}{:02X}{:02X}".format(*rgb)
|
||
|
||
|
||
def hex_to_rgb(s):
|
||
s = s.lstrip("#")
|
||
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
|
||
|
||
|
||
def load_map(path):
|
||
"""Возвращает (QImage, buffer). buffer нужно держать живым во втором случае."""
|
||
|
||
# Штатный загрузчик Qt: нет лимита Pillow, нет лишних копий памяти.
|
||
img = QImage(path)
|
||
if not img.isNull():
|
||
return img, None
|
||
|
||
# Запасной путь через Pillow.
|
||
with Image.open(path) as f:
|
||
pil = f.convert("RGB")
|
||
w, h = pil.size
|
||
raw = pil.tobytes()
|
||
del pil
|
||
|
||
# ВАЖНО: этот конструктор QImage НЕ копирует данные.
|
||
# Буфер raw обязан жить столько же, сколько живёт QImage.
|
||
img = QImage(raw, w, h, w * 3, QImage.Format_RGB888)
|
||
if img.isNull():
|
||
raise RuntimeError("Не удалось декодировать изображение")
|
||
return img, raw
|
||
|
||
|
||
def make_overview(img, max_side=4096):
|
||
"""Маленькая копия для режима «вся карта на экране»."""
|
||
w, h = img.width(), img.height()
|
||
k = min(1.0, max_side / float(max(w, h)))
|
||
if k >= 1.0:
|
||
small = img
|
||
else:
|
||
small = img.scaled(max(1, int(w * k)), max(1, int(h * k)),
|
||
Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||
return QPixmap.fromImage(small)
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# Просмотр карты
|
||
# ----------------------------------------------------------------------
|
||
|
||
class MapViewer(QWidget):
|
||
pixelPicked = pyqtSignal(int, int, tuple) # x, y, (r, g, b)
|
||
zoomChanged = pyqtSignal(float)
|
||
cursorMoved = pyqtSignal(int, int)
|
||
|
||
LOD_SWITCH = 0.25 # ниже этого масштаба рисуем уменьшенную копию
|
||
MAX_SCALE = 64.0
|
||
|
||
def __init__(self, image, raw_buffer, parent=None):
|
||
super().__init__(parent)
|
||
self.image = image
|
||
self._raw = raw_buffer # держим буфер Pillow живым
|
||
self.W = image.width()
|
||
self.H = image.height()
|
||
|
||
print("Размер карты: {} x {} пикселей".format(self.W, self.H))
|
||
print("Создаю уменьшенную копию...")
|
||
self.overview = make_overview(image)
|
||
|
||
self.scale = 1.0 # экранных пикселей на пиксель карты
|
||
self.offset = QPointF(0, 0) # куда смещён левый верхний угол карты
|
||
self._fit_scale = 1.0
|
||
self._fitted = False
|
||
|
||
self._panning = False
|
||
self._pan_last = QPoint()
|
||
self._press_pos = None
|
||
self._press_offset = QPointF()
|
||
self._dragging = False
|
||
self.marker = None
|
||
|
||
self.setMouseTracking(True)
|
||
self.setFocusPolicy(Qt.StrongFocus)
|
||
self.setMinimumSize(200, 120)
|
||
|
||
# ---------- координаты ----------
|
||
|
||
def _to_image(self, pos):
|
||
x = math.floor((pos.x() - self.offset.x()) / self.scale)
|
||
y = math.floor((pos.y() - self.offset.y()) / self.scale)
|
||
return x, y
|
||
|
||
# ---------- масштаб ----------
|
||
|
||
def fitToWindow(self):
|
||
vw, vh = self.width(), self.height()
|
||
if vw < 2 or vh < 2:
|
||
return
|
||
s = min(vw / self.W, vh / self.H)
|
||
self._fit_scale = s
|
||
self.scale = s
|
||
self.offset = QPointF((vw - self.W * s) / 2.0,
|
||
(vh - self.H * s) / 2.0)
|
||
self._fitted = True
|
||
self.update()
|
||
self.zoomChanged.emit(s)
|
||
|
||
def actualSize(self):
|
||
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), 1.0)
|
||
|
||
def zoomIn(self):
|
||
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), self.scale * 1.5)
|
||
|
||
def zoomOut(self):
|
||
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), self.scale / 1.5)
|
||
|
||
def _zoom_at(self, point, new_scale):
|
||
new_scale = max(self._fit_scale * 0.25, min(self.MAX_SCALE, new_scale))
|
||
# Точка карты под курсором должна остаться под курсором.
|
||
ix = (point.x() - self.offset.x()) / self.scale
|
||
iy = (point.y() - self.offset.y()) / self.scale
|
||
self.scale = new_scale
|
||
self.offset = QPointF(point.x() - ix * new_scale,
|
||
point.y() - iy * new_scale)
|
||
self.update()
|
||
self.zoomChanged.emit(new_scale)
|
||
|
||
def resizeEvent(self, event):
|
||
super().resizeEvent(event)
|
||
if not self._fitted:
|
||
self.fitToWindow()
|
||
|
||
# ---------- отрисовка ----------
|
||
|
||
def paintEvent(self, event):
|
||
p = QPainter(self)
|
||
p.fillRect(self.rect(), QColor(45, 45, 48))
|
||
p.setRenderHint(QPainter.SmoothPixmapTransform, self.scale < 1.0)
|
||
|
||
s = self.scale
|
||
ox, oy = self.offset.x(), self.offset.y()
|
||
|
||
if s < self.LOD_SWITCH:
|
||
# Далеко: рисуем маленькую копию, Qt сам обрежет по виджету.
|
||
target = QRectF(ox, oy, self.W * s, self.H * s)
|
||
p.drawPixmap(target, self.overview,
|
||
QRectF(0, 0, self.overview.width(), self.overview.height()))
|
||
else:
|
||
# Близко: берём из полного разрешения ТОЛЬКО видимый прямоугольник.
|
||
x0 = max(0, int(math.floor(-ox / s)))
|
||
y0 = max(0, int(math.floor(-oy / s)))
|
||
x1 = min(self.W, int(math.ceil((self.width() - ox) / s)))
|
||
y1 = min(self.H, int(math.ceil((self.height() - oy) / s)))
|
||
|
||
if x1 > x0 and y1 > y0:
|
||
src = QRect(x0, y0, x1 - x0, y1 - y0)
|
||
target = QRectF(ox + x0 * s, oy + y0 * s,
|
||
(x1 - x0) * s, (y1 - y0) * s)
|
||
p.drawImage(target, self.image, QRectF(src))
|
||
|
||
# Рамка карты
|
||
p.setPen(QPen(QColor(95, 95, 95), 1))
|
||
p.drawRect(QRectF(ox, oy, self.W * s, self.H * s))
|
||
|
||
# Метка последнего выбранного пикселя
|
||
if self.marker is not None and s >= 4:
|
||
mx, my = self.marker
|
||
p.setPen(QPen(QColor(255, 255, 255), 1))
|
||
p.drawRect(QRectF(ox + mx * s, oy + my * s, s, s))
|
||
p.end()
|
||
|
||
# ---------- мышь ----------
|
||
|
||
def wheelEvent(self, event):
|
||
d = event.angleDelta().y()
|
||
if d:
|
||
factor = 1.25 if d > 0 else 1.0 / 1.25
|
||
self._zoom_at(QPointF(event.pos()), self.scale * factor)
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() in (Qt.RightButton, Qt.MiddleButton):
|
||
self._panning = True
|
||
self._pan_last = event.pos()
|
||
self.setCursor(Qt.ClosedHandCursor)
|
||
elif event.button() == Qt.LeftButton:
|
||
self._press_pos = event.pos()
|
||
self._press_offset = QPointF(self.offset)
|
||
self._dragging = False
|
||
|
||
def mouseMoveEvent(self, event):
|
||
if self._panning:
|
||
d = event.pos() - self._pan_last
|
||
self._pan_last = event.pos()
|
||
self.offset = QPointF(self.offset.x() + d.x(),
|
||
self.offset.y() + d.y())
|
||
self.update()
|
||
elif self._press_pos is not None:
|
||
d = event.pos() - self._press_pos
|
||
if d.manhattanLength() > 4:
|
||
self._dragging = True
|
||
if self._dragging:
|
||
self.offset = QPointF(self._press_offset.x() + d.x(),
|
||
self._press_offset.y() + d.y())
|
||
self.update()
|
||
else:
|
||
x, y = self._to_image(event.pos())
|
||
if 0 <= x < self.W and 0 <= y < self.H:
|
||
self.cursorMoved.emit(x, y)
|
||
else:
|
||
self.cursorMoved.emit(-1, -1)
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
if event.button() in (Qt.RightButton, Qt.MiddleButton):
|
||
self._panning = False
|
||
self.setCursor(Qt.ArrowCursor)
|
||
elif event.button() == Qt.LeftButton and self._press_pos is not None:
|
||
if not self._dragging:
|
||
self._pick(event.pos())
|
||
self._press_pos = None
|
||
self._dragging = False
|
||
|
||
def _pick(self, pos):
|
||
x, y = self._to_image(pos)
|
||
if 0 <= x < self.W and 0 <= y < self.H:
|
||
c = self.image.pixelColor(x, y)
|
||
self.marker = (x, y)
|
||
self.update()
|
||
self.pixelPicked.emit(x, y, (c.red(), c.green(), c.blue()))
|
||
|
||
def keyPressEvent(self, event):
|
||
if event.key() in (Qt.Key_0, Qt.Key_F):
|
||
self.fitToWindow()
|
||
elif event.key() == Qt.Key_1:
|
||
self.actualSize()
|
||
elif event.key() in (Qt.Key_Plus, Qt.Key_Equal):
|
||
self.zoomIn()
|
||
elif event.key() == Qt.Key_Minus:
|
||
self.zoomOut()
|
||
else:
|
||
super().keyPressEvent(event)
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# Окно
|
||
# ----------------------------------------------------------------------
|
||
|
||
class MainWindow(QMainWindow):
|
||
def __init__(self, path):
|
||
super().__init__()
|
||
self.setWindowTitle("Region Mapper — " + os.path.basename(path))
|
||
self.resize(1400, 850)
|
||
|
||
# Обычный dict: в Python 3.7+ сохраняет порядок вставки.
|
||
self.mapping = {} # name -> "#RRGGBB"
|
||
self.by_color = {} # (r, g, b) -> name
|
||
self.current_rgb = None
|
||
self._shown_once = False
|
||
|
||
image, raw = load_map(path)
|
||
self.viewer = MapViewer(image, raw)
|
||
self.viewer.pixelPicked.connect(self.on_pixel)
|
||
self.viewer.zoomChanged.connect(
|
||
lambda s: self.zoom_label.setText("Масштаб: {:.4f}×".format(s)))
|
||
self.viewer.cursorMoved.connect(
|
||
lambda x, y: self.coord_label.setText("X: {} Y: {}".format(x, y)))
|
||
|
||
self._build_ui()
|
||
|
||
def showEvent(self, event):
|
||
super().showEvent(event)
|
||
if not self._shown_once:
|
||
self._shown_once = True
|
||
# Финальный фит уже после того, как layout раздал размеры.
|
||
QTimer.singleShot(0, self.viewer.fitToWindow)
|
||
|
||
def _build_ui(self):
|
||
central = QWidget()
|
||
self.setCentralWidget(central)
|
||
root = QHBoxLayout(central)
|
||
root.setContentsMargins(6, 6, 6, 6)
|
||
splitter = QSplitter(Qt.Horizontal)
|
||
|
||
# --- слева: тулбар + карта ---
|
||
left = QWidget()
|
||
lv = QVBoxLayout(left)
|
||
lv.setContentsMargins(0, 0, 0, 0)
|
||
tb = QHBoxLayout()
|
||
for text, slot in (("Вписать (0)", self.viewer.fitToWindow),
|
||
("1:1", self.viewer.actualSize),
|
||
("+", self.viewer.zoomIn),
|
||
("−", self.viewer.zoomOut)):
|
||
b = QPushButton(text)
|
||
b.clicked.connect(slot)
|
||
tb.addWidget(b)
|
||
self.zoom_label = QLabel("Масштаб: —")
|
||
tb.addWidget(self.zoom_label)
|
||
tb.addStretch()
|
||
lv.addLayout(tb)
|
||
lv.addWidget(self.viewer, 1)
|
||
splitter.addWidget(left)
|
||
|
||
# --- справа: панель разметки ---
|
||
panel = QWidget()
|
||
pv = QVBoxLayout(panel)
|
||
pv.setContentsMargins(8, 8, 8, 8)
|
||
pv.setSpacing(6)
|
||
|
||
pv.addWidget(QLabel("Текущий пиксель:"))
|
||
row = QHBoxLayout()
|
||
self.swatch = QLabel()
|
||
self.swatch.setFixedSize(64, 44)
|
||
self.swatch.setStyleSheet("background:#808080; border:1px solid #555;")
|
||
row.addWidget(self.swatch)
|
||
self.color_info = QLabel("Кликните ЛКМ по карте")
|
||
self.color_info.setWordWrap(True)
|
||
row.addWidget(self.color_info, 1)
|
||
pv.addLayout(row)
|
||
|
||
pv.addSpacing(10)
|
||
pv.addWidget(QLabel("Название региона:"))
|
||
self.name_input = QLineEdit()
|
||
self.name_input.setPlaceholderText("Например: pepene")
|
||
self.name_input.returnPressed.connect(self.commit)
|
||
pv.addWidget(self.name_input)
|
||
|
||
btn = QPushButton("Назначить / обновить цвет [Enter]")
|
||
btn.clicked.connect(self.commit)
|
||
pv.addWidget(btn)
|
||
|
||
pv.addSpacing(12)
|
||
pv.addWidget(QLabel("Регионы (порядок = порядок в JSON):"))
|
||
self.list_widget = QListWidget()
|
||
self.list_widget.itemDoubleClicked.connect(
|
||
lambda it: self.rename_region(it.data(Qt.UserRole)))
|
||
pv.addWidget(self.list_widget, 1)
|
||
|
||
row2 = QHBoxLayout()
|
||
b1 = QPushButton("Переименовать")
|
||
b1.clicked.connect(lambda: self.rename_region())
|
||
b2 = QPushButton("Удалить")
|
||
b2.clicked.connect(self.delete_region)
|
||
row2.addWidget(b1)
|
||
row2.addWidget(b2)
|
||
pv.addLayout(row2)
|
||
|
||
pv.addSpacing(12)
|
||
b_open = QPushButton("Открыть существующий JSON")
|
||
b_open.clicked.connect(self.load_json)
|
||
pv.addWidget(b_open)
|
||
|
||
b_save = QPushButton("Сохранить JSON [Ctrl+S]")
|
||
b_save.setStyleSheet("background:#2e7d32; color:white; "
|
||
"font-weight:bold; padding:6px;")
|
||
b_save.clicked.connect(self.save_json)
|
||
pv.addWidget(b_save)
|
||
|
||
splitter.addWidget(panel)
|
||
splitter.setStretchFactor(0, 1)
|
||
splitter.setStretchFactor(1, 0)
|
||
splitter.setSizes([1000, 330])
|
||
root.addWidget(splitter)
|
||
|
||
self.coord_label = QLabel("X: — Y: —")
|
||
self.statusBar().addPermanentWidget(self.coord_label)
|
||
self.statusBar().showMessage(
|
||
"ЛКМ — выбрать пиксель; ЛКМ-драг / ПКМ — панорама; "
|
||
"колесо — зум; 0 — вписать; 1 — 1:1")
|
||
|
||
QShortcut(QKeySequence("Ctrl+S"), self, self.save_json)
|
||
|
||
# ---------- выбор пикселя ----------
|
||
|
||
def on_pixel(self, x, y, rgb):
|
||
self.current_rgb = rgb
|
||
hexv = rgb_to_hex(rgb)
|
||
self.swatch.setStyleSheet("background:{}; border:1px solid #555;".format(hexv))
|
||
|
||
text = "{}\nRGB ({}, {}, {})\nПиксель ({}, {})".format(
|
||
hexv, rgb[0], rgb[1], rgb[2], x, y)
|
||
|
||
owner = self.by_color.get(rgb)
|
||
if owner:
|
||
text += "\nУже назначен: " + owner
|
||
self.name_input.setText(owner) # сразу готово к правке
|
||
self._select_in_list(owner)
|
||
else:
|
||
self.name_input.clear()
|
||
|
||
self.color_info.setText(text)
|
||
self.name_input.setFocus()
|
||
self.name_input.selectAll()
|
||
|
||
# ---------- запись / правка маппинга ----------
|
||
|
||
def commit(self):
|
||
name = self.name_input.text().strip()
|
||
if not name:
|
||
QMessageBox.warning(self, "Название", "Введите название региона.")
|
||
return
|
||
if self.current_rgb is None:
|
||
QMessageBox.warning(self, "Цвет", "Сначала кликните по пикселю на карте.")
|
||
return
|
||
|
||
rgb = self.current_rgb
|
||
hexv = rgb_to_hex(rgb)
|
||
|
||
# Цвет уже занят другим регионом?
|
||
owner = self.by_color.get(rgb)
|
||
if owner is not None and owner != name:
|
||
ret = QMessageBox.question(
|
||
self, "Цвет занят",
|
||
"Цвет {} уже назначен региону «{}».\n"
|
||
"Переназначить его региону «{}»?".format(hexv, owner, name),
|
||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||
if ret != QMessageBox.Yes:
|
||
return
|
||
self.mapping.pop(owner, None)
|
||
self.by_color.pop(rgb, None)
|
||
|
||
# Освобождаем старый цвет, если регион редактируется.
|
||
if name in self.mapping:
|
||
old_rgb = hex_to_rgb(self.mapping[name])
|
||
if self.by_color.get(old_rgb) == name:
|
||
self.by_color.pop(old_rgb, None)
|
||
|
||
# Присваивание СУЩЕСТВУЮЩЕМУ ключу не меняет его позицию в dict.
|
||
self.mapping[name] = hexv
|
||
self.by_color[rgb] = name
|
||
|
||
self._rebuild_list(select=name)
|
||
self.name_input.clear()
|
||
self.name_input.setFocus()
|
||
|
||
def _rebuild_list(self, select=None):
|
||
self.list_widget.clear()
|
||
for i, (name, hexv) in enumerate(self.mapping.items(), 1):
|
||
it = QListWidgetItem("{:>3}. {} {}".format(i, name, hexv))
|
||
it.setData(Qt.UserRole, name)
|
||
it.setBackground(QColor(hexv))
|
||
c = QColor(hexv)
|
||
bright = 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()
|
||
it.setForeground(QColor(20, 20, 20) if bright > 140
|
||
else QColor(245, 245, 245))
|
||
self.list_widget.addItem(it)
|
||
if select is not None:
|
||
self._select_in_list(select)
|
||
|
||
def _select_in_list(self, name):
|
||
for i in range(self.list_widget.count()):
|
||
if self.list_widget.item(i).data(Qt.UserRole) == name:
|
||
self.list_widget.setCurrentRow(i)
|
||
return
|
||
|
||
def rename_region(self, name=None):
|
||
if name is None:
|
||
it = self.list_widget.currentItem()
|
||
if it is None:
|
||
return
|
||
name = it.data(Qt.UserRole)
|
||
|
||
new, ok = QInputDialog.getText(self, "Переименовать регион",
|
||
"Новое название:", QLineEdit.Normal, name)
|
||
if not ok:
|
||
return
|
||
new = new.strip()
|
||
if not new or new == name:
|
||
return
|
||
if new in self.mapping:
|
||
QMessageBox.warning(self, "Дубликат", "Регион с таким названием уже есть.")
|
||
return
|
||
|
||
# Пересобираем dict, сохраняя позицию переименованного элемента.
|
||
reordered = {}
|
||
for k, v in self.mapping.items():
|
||
reordered[new if k == name else k] = v
|
||
self.mapping = reordered
|
||
|
||
rgb = hex_to_rgb(self.mapping[new])
|
||
self.by_color.pop(rgb, None)
|
||
self.by_color[rgb] = new
|
||
self._rebuild_list(select=new)
|
||
|
||
def delete_region(self):
|
||
it = self.list_widget.currentItem()
|
||
if it is None:
|
||
return
|
||
name = it.data(Qt.UserRole)
|
||
hexv = self.mapping.pop(name, None)
|
||
if hexv is not None:
|
||
rgb = hex_to_rgb(hexv)
|
||
if self.by_color.get(rgb) == name:
|
||
self.by_color.pop(rgb, None)
|
||
self._rebuild_list()
|
||
|
||
# ---------- JSON ----------
|
||
|
||
def save_json(self):
|
||
if not self.mapping:
|
||
QMessageBox.information(self, "Пусто", "Сначала добавьте регион.")
|
||
return
|
||
path, _ = QFileDialog.getSaveFileName(self, "Сохранить маппинг",
|
||
"regions.json", "JSON (*.json)")
|
||
if not path:
|
||
return
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(self.mapping, f, ensure_ascii=False, indent=2)
|
||
f.write("\n")
|
||
self.statusBar().showMessage(
|
||
"Сохранено регионов: {} → {}".format(len(self.mapping), path), 8000)
|
||
|
||
def load_json(self):
|
||
path, _ = QFileDialog.getOpenFileName(self, "Открыть маппинг",
|
||
"", "JSON (*.json)")
|
||
if not path:
|
||
return
|
||
try:
|
||
with open(path, encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
if not isinstance(data, dict):
|
||
raise ValueError("Ожидался JSON-объект")
|
||
|
||
mapping, by_color = {}, {}
|
||
for name, value in data.items():
|
||
if not isinstance(value, str):
|
||
continue
|
||
v = value.strip()
|
||
if len(v) != 7 or not v.startswith("#"):
|
||
continue
|
||
try:
|
||
rgb = hex_to_rgb(v)
|
||
except ValueError:
|
||
continue
|
||
name = str(name)
|
||
if rgb in by_color:
|
||
continue
|
||
mapping[name] = rgb_to_hex(rgb)
|
||
by_color[rgb] = name
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Ошибка чтения", str(e))
|
||
return
|
||
|
||
self.mapping = mapping
|
||
self.by_color = by_color
|
||
self._rebuild_list()
|
||
self.statusBar().showMessage("Загружено регионов: {}".format(len(mapping)), 5000)
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
|
||
def main():
|
||
app = QApplication(sys.argv)
|
||
app.setApplicationName("Region Mapper")
|
||
|
||
if len(sys.argv) > 1:
|
||
path = sys.argv[1]
|
||
else:
|
||
path, _ = QFileDialog.getOpenFileName(
|
||
None, "Открыть карту", "",
|
||
"Изображения (*.bmp *.png *.jpg *.jpeg *.tif *.tiff);;Все файлы (*)")
|
||
if not path:
|
||
return
|
||
|
||
if not os.path.isfile(path):
|
||
QMessageBox.critical(None, "Файл не найден", path)
|
||
return
|
||
|
||
try:
|
||
win = MainWindow(path)
|
||
except Exception as exc:
|
||
QMessageBox.critical(None, "Не удалось открыть карту", repr(exc))
|
||
raise
|
||
|
||
win.show()
|
||
sys.exit(app.exec_())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |