commit 58aaf933924595e795a33a402aa4a73eb5be35d1 Author: Vladimir Date: Wed Aug 5 02:25:56 2026 +0300 init: начало работы над приложением с использованием wxPython diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9df9540 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.pybuild + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ \ No newline at end of file diff --git a/build_scripts/build_exe.py b/build_scripts/build_exe.py new file mode 100644 index 0000000..54ef942 --- /dev/null +++ b/build_scripts/build_exe.py @@ -0,0 +1,67 @@ +import argparse +import os +import shutil +import subprocess +import sys + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +ENTRY_POINT = os.path.join(PROJECT_ROOT, "src", "mind_reader", "app.py") + + +def build(): + cmd = [ + sys.executable, + "-m", + "PyInstaller", + "--noconfirm", + "--clean", + "--onedir", + "--windowed", + "--name", + "MindReader", + "--paths", + os.path.join(PROJECT_ROOT, "src"), + ENTRY_POINT, + ] + + result = subprocess.run(cmd, cwd=PROJECT_ROOT) + + if result.returncode == 0: + dist_path = os.path.join(PROJECT_ROOT, "dist", "MindReader") + print("\nBuild completed!") + print(f"{dist_path}") + else: + print("\nError!") + + +def clean(): + print("Cleaning...") + + dirs_to_remove = [ + os.path.join(PROJECT_ROOT, "build"), + os.path.join(PROJECT_ROOT, "dist"), + ] + + for d in dirs_to_remove: + if os.path.exists(d): + shutil.rmtree(d) + print(f"{d} - deleted") + + for item in os.listdir(PROJECT_ROOT): + if item.endswith(".spec"): + spec_path = os.path.join(PROJECT_ROOT, item) + os.remove(spec_path) + print(f"{spec_path} - deleted") + + print("Cleaning completed!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--clean-only", action="store_true") + args = parser.parse_args() + + if args.clean_only: + clean() + else: + build() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..82432ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "mind-reader" +version = "0.1.0" +description = "Сетевая игра на угадывание последовательностей" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "wxPython>=4.2.0", +] + +[project.scripts] +mind-reader = "mind_reader.app:main" \ No newline at end of file diff --git a/src/mind_reader/__init__.py b/src/mind_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mind_reader/__main__.py b/src/mind_reader/__main__.py new file mode 100644 index 0000000..98f4bfa --- /dev/null +++ b/src/mind_reader/__main__.py @@ -0,0 +1,4 @@ +from mind_reader.app import main + +if __name__ == "__main__": + main() diff --git a/src/mind_reader/app.py b/src/mind_reader/app.py new file mode 100644 index 0000000..6b3abed --- /dev/null +++ b/src/mind_reader/app.py @@ -0,0 +1,39 @@ +import sys + +import wx + +from mind_reader.ui.main_frame import MainFrame + + +def enable_high_dpi_support(): + if sys.platform == "win32": + try: + import ctypes + + ctypes.windll.shcore.SetProcessDpiAwareness(2) + except Exception: + try: + import ctypes + + ctypes.windll.user32.SetProcessDPIAware() + except Exception: + pass + + +class MindReaderApp(wx.App): + def OnInit(self) -> bool: + self.frame = MainFrame() + self.frame.Show(True) + self.SetTopWindow(self.frame) + return True + + +def main(): + enable_high_dpi_support() + + app = MindReaderApp(redirect=False) + app.MainLoop() + + +if __name__ == "__main__": + main() diff --git a/src/mind_reader/ui/__init__.py b/src/mind_reader/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mind_reader/ui/dialogs/__init__.py b/src/mind_reader/ui/dialogs/__init__.py new file mode 100644 index 0000000..6d8984c --- /dev/null +++ b/src/mind_reader/ui/dialogs/__init__.py @@ -0,0 +1,7 @@ +from mind_reader.ui.dialogs.client_dialog import ClientConnectDialog +from mind_reader.ui.dialogs.server_dialog import ServerStartDialog + +__all__ = [ + "ClientConnectDialog", + "ServerStartDialog", +] diff --git a/src/mind_reader/ui/dialogs/client_dialog.py b/src/mind_reader/ui/dialogs/client_dialog.py new file mode 100644 index 0000000..729cd5c --- /dev/null +++ b/src/mind_reader/ui/dialogs/client_dialog.py @@ -0,0 +1,84 @@ +import wx + + +class ClientConnectDialog(wx.Dialog): + def __init__(self, parent: wx.Window): + super().__init__( + parent=parent, + title="Подключение к серверу", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + ) + + self._init_ui() + + self.Fit() + calculated_size = self.GetSize() + self.SetMinSize(calculated_size) + self.CenterOnParent() + + def _init_ui(self): + main_sizer = wx.BoxSizer(wx.VERTICAL) + grid_sizer = wx.FlexGridSizer(rows=2, cols=2, vgap=10, hgap=10) + grid_sizer.AddGrowableCol(1, 1) + + label_name = wx.StaticText(self, label="Ваше имя:") + self.text_name = wx.TextCtrl(self, value="Игрок1") + + grid_sizer.Add(label_name, 0, wx.ALIGN_CENTER_VERTICAL) + grid_sizer.Add(self.text_name, 1, wx.EXPAND) + + label_ip = wx.StaticText(self, label="IP адрес сервера:") + self.text_ip = wx.TextCtrl(self, value="127.0.0.1") + + grid_sizer.Add(label_ip, 0, wx.ALIGN_CENTER_VERTICAL) + grid_sizer.Add(self.text_ip, 1, wx.EXPAND) + + main_sizer.Add(grid_sizer, 0, wx.EXPAND | wx.ALL, 15) + + main_sizer.Add( + wx.StaticLine(self), 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10 + ) + + button_sizer = wx.StdDialogButtonSizer() + self.button_connect = wx.Button(self, wx.ID_OK, label="Подключиться") + self.button_cancel = wx.Button(self, wx.ID_CANCEL, label="Отмена") + + button_sizer.AddButton(self.button_connect) + button_sizer.AddButton(self.button_cancel) + button_sizer.Realize() + + main_sizer.Add(button_sizer, 0, wx.EXPAND | wx.ALL, 10) + + self.SetSizer(main_sizer) + + self.button_connect.Bind(wx.EVT_BUTTON, self._on_connect) + + def _on_connect(self, event): + name = self.text_name.GetValue().strip() + ip = self.text_ip.GetValue().strip() + + if not name: + wx.MessageBox( + "Пожалуйста, введите ваше имя", + "Ошибка", + wx.OK | wx.ICON_WARNING, + self, + ) + return + + if not ip: + wx.MessageBox( + "Пожалуйста, введите IP адрес сервера", + "Ошибка", + wx.OK | wx.ICON_WARNING, + self, + ) + return + + event.Skip() + + def get_data(self) -> tuple[str, str]: + return ( + self.text_name.GetValue().strip(), + self.text_ip.GetValue().strip(), + ) diff --git a/src/mind_reader/ui/dialogs/server_dialog.py b/src/mind_reader/ui/dialogs/server_dialog.py new file mode 100644 index 0000000..77b45f4 --- /dev/null +++ b/src/mind_reader/ui/dialogs/server_dialog.py @@ -0,0 +1,99 @@ +import socket + +import wx + + +def get_available_ip_addresses() -> list[str]: + ip_list = [ + "127.0.0.1", + ] + + try: + hostname = socket.gethostname() + for ip in socket.gethostbyname_ex(hostname)[2]: + if ip not in ip_list: + ip_list.append(ip) + except Exception: + pass + return ip_list + + +class ServerStartDialog(wx.Dialog): + def __init__(self, parent: wx.Window): + super().__init__( + parent=parent, + title="Запуск сервера", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + ) + + self._init_ui() + + self.Fit() + calculated_size = self.GetSize() + self.SetMinSize(calculated_size) + self.CenterOnParent() + + def _init_ui(self): + main_sizer = wx.BoxSizer(wx.VERTICAL) + grid_sizer = wx.FlexGridSizer(rows=2, cols=2, vgap=10, hgap=10) + grid_sizer.AddGrowableCol(1, 1) + + label_name = wx.StaticText(self, label="Ваше имя:") + self.text_name = wx.TextCtrl(self, value="Игрок2") + grid_sizer.Add(label_name, 0, wx.ALIGN_CENTER_VERTICAL) + grid_sizer.Add(self.text_name, 1, wx.EXPAND) + + label_ip = wx.StaticText(self, label="IP адрес для запуска:") + available_ips = get_available_ip_addresses() + self.combo_ip = wx.ComboBox( + self, + choices=available_ips, + style=wx.CB_READONLY, + ) + if available_ips: + self.combo_ip.SetSelection(0) + + grid_sizer.Add(label_ip, 0, wx.ALIGN_CENTER_VERTICAL) + grid_sizer.Add(self.combo_ip, 1, wx.EXPAND) + + main_sizer.Add(grid_sizer, 0, wx.EXPAND | wx.ALL, 15) + + main_sizer.Add( + wx.StaticLine(self), 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10 + ) + + button_sizer = wx.StdDialogButtonSizer() + self.button_start = wx.Button( + self, wx.ID_OK, label="Ожидать подключение" + ) + self.button_cancel = wx.Button(self, wx.ID_CANCEL, label="Отмена") + + button_sizer.AddButton(self.button_start) + button_sizer.AddButton(self.button_cancel) + button_sizer.Realize() + + main_sizer.Add(button_sizer, 0, wx.EXPAND | wx.ALL, 10) + + self.SetSizer(main_sizer) + + self.button_start.Bind(wx.EVT_BUTTON, self._on_start) + + def _on_start(self, event): + name = self.text_name.GetValue().strip() + + if not name: + wx.MessageBox( + "Пожалуйста, введите ваше имя", + "Ошибка", + wx.OK | wx.ICON_WARNING, + self, + ) + return + + event.Skip() + + def get_data(self) -> tuple[str, str]: + return ( + self.text_name.GetValue().strip(), + self.combo_ip.GetValue(), + ) diff --git a/src/mind_reader/ui/main_frame.py b/src/mind_reader/ui/main_frame.py new file mode 100644 index 0000000..a0eaa98 --- /dev/null +++ b/src/mind_reader/ui/main_frame.py @@ -0,0 +1,53 @@ +import wx + +from mind_reader.ui.dialogs import ClientConnectDialog, ServerStartDialog +from mind_reader.ui.menu_bar import AppMenuBar + + +class MainFrame(wx.Frame): + def __init__(self): + super().__init__( + parent=None, + id=wx.ID_ANY, + title="Mind Reader", + size=wx.Size(700, 500), + style=wx.DEFAULT_FRAME_STYLE, + ) + self.Center() + + self.menu_bar = AppMenuBar(self) + self.SetMenuBar(self.menu_bar) + + self.CreateStatusBar(1) + self.SetStatusText("Подключение отсутствует") + + self._bind_menu_events() + + def _bind_menu_events(self): + self.Bind( + wx.EVT_MENU, self.on_client_connect, self.menu_bar.item_client + ) + self.Bind(wx.EVT_MENU, self.on_server_start, self.menu_bar.item_server) + self.Bind( + wx.EVT_MENU, self.on_disconnect, self.menu_bar.item_disconnect + ) + + def on_client_connect(self, event): + d = ClientConnectDialog(self) + if d.ShowModal() == wx.ID_OK: + name, ip = d.get_data() + self.menu_bar.set_connected_state(True) + self.SetStatusText(f"Подключено к {ip} | Ваше имя {name}") + d.Destroy() + + def on_server_start(self, event): + d = ServerStartDialog(self) + if d.ShowModal() == wx.ID_OK: + name, ip = d.get_data() + self.menu_bar.set_connected_state(True) + self.SetStatusText(f"Сервер запущен на {ip} | Ваше имя {name}") + d.Destroy() + + def on_disconnect(self, event): + self.menu_bar.set_connected_state(False) + self.SetStatusText("Подключение отсутствует") diff --git a/src/mind_reader/ui/menu_bar.py b/src/mind_reader/ui/menu_bar.py new file mode 100644 index 0000000..b713e8b --- /dev/null +++ b/src/mind_reader/ui/menu_bar.py @@ -0,0 +1,85 @@ +import wx + + +class AppMenuBar(wx.MenuBar): + def __init__(self, parent: wx.Frame): + super().__init__() + self.parent = parent + + self._create_file_menu() + self._create_connection_menu() + self._create_help_menu() + + def _create_file_menu(self): + self.file_menu = wx.Menu() + + self.item_open = self.file_menu.Append( + wx.ID_OPEN, + "&Открыть...\tCtrl+O", + "Открыть файл с результатами сессий", + ) + + self.item_save = self.file_menu.Append( + wx.ID_SAVE, + "&Сохранить\tCtrl+S", + "Сохранить текущие результаты сессий", + ) + + self.item_save_as = self.file_menu.Append( + wx.ID_SAVEAS, + "Сохранить &как...\tCtrl+Shift+S", + "Сохранить результаты сессий в новый файл", + ) + + self.file_menu.AppendSeparator() + + self.item_exit = self.file_menu.Append( + wx.ID_EXIT, + "&Выход\tAlt+F4", + "Завершить работу программы", + ) + + self.Append(self.file_menu, "&Файл") + + def _create_connection_menu(self): + self.conn_menu = wx.Menu() + + self.item_client = self.conn_menu.Append( + wx.ID_ANY, + "&Клиент...", + "Подключиться к серверу", + ) + + self.item_server = self.conn_menu.Append( + wx.ID_ANY, + "&Сервер...", + "Запустить сервер", + ) + + self.conn_menu.AppendSeparator() + + self.item_disconnect = self.conn_menu.Append( + wx.ID_ANY, + "&Разорвать подключение", + "Завершить сетевое соединение", + ) + + self.item_disconnect.Enable(False) + + self.Append(self.conn_menu, "&Подключение") + + def _create_help_menu(self): + self.help_menu = wx.Menu() + + self.item_about = self.help_menu.Append( + wx.ID_ABOUT, + "&О программе...\tF1", + "Информация о программе", + ) + + self.Append(self.help_menu, "&Справка") + + def set_connected_state(self, is_connected: bool): + self.item_client.Enable(not is_connected) + self.item_server.Enable(not is_connected) + self.item_disconnect.Enable(is_connected)