init: начало работы над приложением с использованием wxPython

This commit is contained in:
2026-08-05 02:25:56 +03:00
commit 58aaf93392
12 changed files with 495 additions and 0 deletions
+41
View File
@@ -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/
+67
View File
@@ -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()
+16
View File
@@ -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"
View File
+4
View File
@@ -0,0 +1,4 @@
from mind_reader.app import main
if __name__ == "__main__":
main()
+39
View File
@@ -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()
View File
+7
View File
@@ -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",
]
@@ -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(),
)
@@ -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(),
)
+53
View File
@@ -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("Подключение отсутствует")
+85
View File
@@ -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)