# Конвертор чисел p₁ → p₂: Полное Руководство по Архитектуре и Реализации Это детализированная документация учебного проекта по разработке приложения для перевода действительных чисел из одной системы счисления в другую (с основанием от 2 до 16). Проект реализован на **Python 3.12+** с использованием графической библиотеки **PyQt6**. --- ## 📋 Оглавление 1. [Общее Описание](#общее-описание) 2. [Быстрый Старт](#быстрый-старт) 3. [Архитектура Приложения](#архитектура-приложения) 4. [Детальное Описание Модулей](#детальное-описание-модулей) 5. [Алгоритмы Конвертации](#алгоритмы-конвертации) 6. [Машина Состояний](#машина-состояний) 7. [Расчёт Точности](#расчёт-точности) 8. [Обработка Ввода](#обработка-ввода) 9. [Журнал Операций](#журнал-операций) 10. [Стилизация UI](#стилизация-ui) 11. [Таблица Соответствия ТЗ](#таблица-соответствия-тз) --- ## 📖 Общее Описание ### Назначение Приложения Приложение предназначено для: - Перевода действительных чисел (с дробной частью) между системами счисления с основаниями от 2 до 16 - Визуального отображения процесса конвертации - Ведения журнала всех выполненных операций - Интуитивного ввода чисел через экранные кнопки или клавиатуру ### Поддерживаемые Системы Счисления | Основание | Название | Допустимые цифры | |-----------|----------------|----------------------------| | 2 | Двоичная | 0, 1 | | 8 | Восьмеричная | 0-7 | | 10 | Десятичная | 0-9 | | 16 | Шестнадцатеричная | 0-9, A-F | **Примечание:** Приложение динамически блокирует кнопки с цифрами, недопустимыми для выбранного основания p₁. --- ## 🚀 Быстрый Старт ### Требования - Python 3.12 или выше - Менеджер пакетов `uv` - Библиотека PyQt6 ### Установка и Запуск ```bash # Установка зависимостей и запуск приложения uv run main.py ``` ### Структура Файлов ``` lab1/ ├── main.py # Точка входа, GUI на PyQt6 ├── Control_.py # Контроллер, машина состояний ├── Editor.py # Редактор чисел (ввод, удаление, очистка) ├── Conver_10_p.py # Конвертация: десятичная → p-ичная ├── Conver_p_10.py # Конвертация: p-ичная → десятичная ├── History.py # Журнал операций ├── pyproject.toml # Конфигурация проекта ├── uv.lock # Заблокированные версии зависимостей ├── README.md # Эта документация └── task.pdf # Техническое задание ``` --- ## 🏗️ Архитектура Приложения Приложение построено по принципу **разделения ответственности** (Separation of Concerns) с чётким разделением на слои: ``` ┌─────────────────────────────────────────────────────────────┐ │ GUI Layer (main.py) │ │ MainForm, HistoryForm, AboutForm │ │ - Отображение данных │ │ - Обработка событий UI (клики, клавиатура) │ │ - Стилизация │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Controller Layer (Control_.py) │ │ Control_ класс │ │ - Координация между слоями │ │ - Управление состояниями (EDITING / CONVERTED) │ │ - Расчёт точности конвертации │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Logic Layer (4 модуля) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Editor │ │ Conver_p_10 │ │ Conver_10_p │ │ │ │ (ввод) │ │ (p → 10) │ │ (10 → p) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ┌─────────────┐ │ │ │ History │ │ │ │ (журнал) │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Поток Данных при Конвертации ``` 1. Пользователь вводит число через GUI │ ▼ 2. MainForm.keyPressEvent() или do_command() │ ▼ 3. Control_.do_cmnd() → Editor.do_edit() │ ▼ 4. Пользователь нажимает EXEC (cmd=19) │ ▼ 5. Conver_p_10.dval() — перевод в десятичную │ ▼ 6. Conver_10_p.Do() — перевод в целевую систему p₂ │ ▼ 7. History.add_record() — сохранение в журнал │ ▼ 8. Обновление GUI (label_result) ``` --- ## 📦 Детальное Описание Модулей ### 1. main.py — Графический Интерфейс (GUI) **Ответственность:** Отображение интерфейса, обработка пользовательского ввода, стилизация. #### Классы ##### `MainForm(QMainWindow)` — Основное Окно **Атрибуты:** | Атрибут | Тип | Описание | |---------|-----|----------| | `ctl` | `Control_` | Ссылка на контроллер | | `label_source` | `QLabel` | Отображение текущего вводимого числа | | `label_result` | `QLabel` | Отображение результата конвертации | | `spin_p1`, `spin_p2` | `QSpinBox` | Поля выбора оснований систем счисления | | `slider_p1`, `slider_p2` | `QSlider` | Слайдеры для плавного изменения оснований | | `buttons` | `list[QPushButton]` | Кнопки цифр 0-9, A-F | **Методы:** | Метод | Назначение | |-------|------------| | `__init__()` | Инициализация окна, создание UI, применение стилей | | `apply_styles()` | Применение CSS-подобных стилей (цветовая схема Catppuccin Mocha) | | `init_ui()` | Создание всех виджетов и их компоновка | | `p1_changed(val)` | Обработчик изменения p₁ через спинбокс | | `p1_slider_changed(val)` | Обработчик изменения p₁ через слайдер | | `p2_changed(val)` | Обработчик изменения p₂ через спинбокс | | `p2_slider_changed(val)` | Обработчик изменения p₂ через слайдер | | `update_buttons()` | Блокировка кнопок с недопустимыми цифрами для текущего p₁ | | `do_command(j)` | Обработка нажатия кнопки (0-19) | | `show_history()` | Открытие диалога истории | | `show_about()` | Открытие диалога «О программе» | | `keyPressEvent(event)` | Обработка нажатий клавиатуры | **Код клавиш (Команды):** | Код | Команда | Описание | |-----|---------|----------| | 0-15 | Digit | Ввод цифры (0-9, A=10, B=11, ..., F=15) | | 16 | Delim | Добавление десятичного разделителя (.) | | 17 | BS | Удаление последнего символа (Backspace) | | 18 | CL | Полная очистка числа (Clear) | | 19 | EXEC | Выполнение конвертации | **Детали реализации `keyPressEvent`:** ```python def keyPressEvent(self, event): key = event.text().upper() # Цифры 0-9 if "0" <= key <= "9": val = ord(key) - ord("0") if val < self.ctl.pin: # Проверка допустимости цифры self.do_command(val) # Буквы A-F для шестнадцатеричной системы elif "A" <= key <= "F": val = ord(key) - ord("A") + 10 if val < self.ctl.pin: self.do_command(val) # Разделитель (точка или запятая) elif key in (".", ","): self.do_command(16) # Backspace — удаление символа elif event.key() == Qt.Key.Key_Backspace: self.do_command(17) # Delete — полная очистка elif event.key() == Qt.Key.Key_Delete: self.do_command(18) # Enter — выполнение конвертации elif event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): self.do_command(19) ``` ##### `HistoryForm(QDialog)` — Окно Истории **Ответственность:** Отображение списка всех выполненных операций конвертации. **Реализация:** - Создает `QListWidget` для отображения записей - Итерирует по всем записям через `history.count()` и `history.get_record(i)` - Фиксированный размер 400×300 пикселей ##### `AboutForm(QDialog)` — Окно «О Программе» **Ответственность:** Отображение информации об авторе и приложении. **Реализация:** - Заголовок: «Конвертор p1_p2» - Автор: «Фунтиков Илья» - Фиксированный размер 300×150 пикселей --- ### 2. Control_.py — Контроллер **Ответственность:** Координация работы всех модулей, управление состояниями, расчёт точности. #### Класс `State(Enum)` — Перечисление Состояний ```python class State(Enum): EDITING = 1 # Пользователь вводит число CONVERTED = 2 # Конвертация выполнена, результат показан ``` #### Класс `Control_` **Атрибуты:** | Атрибут | Тип | Описание | Значение по умолчанию | |---------|-----|----------|----------------------| | `ed` | `Editor` | Экземпляр редактора чисел | — | | `his` | `History` | Экземпляр журнала операций | — | | `_pin` | `int` | Исходное основание системы счисления (p₁) | 10 | | `_pout` | `int` | Целевое основание системы счисления (p₂) | 16 | | `_state` | `State` | Текущее состояние автомата | `State.EDITING` | **Свойства (Properties):** | Свойство | Тип | Доступ | Описание | |----------|-----|--------|----------| | `pin` | `int` | read/write | Основание исходной системы | | `pout` | `int` | read/write | Основание целевой системы | | `state` | `State` | read/write | Текущее состояние | **Методы:** ##### `do_cmnd(j: int) -> str` Обработчик команд от GUI. **Логика работы:** ```python def do_cmnd(self, j: int) -> str: if j == 19: # EXEC — команда конвертации # Шаг 1: Перевод из p₁ в десятичную систему r = Conver_p_10.dval(self.ed.number, self._pin) # Шаг 2: Перевод из десятичной в p₂ с расчётом точности res = Conver_10_p.Do(r, self._pout, self.acc()) # Шаг 3: Сохранение в историю self.his.add_record(self._pin, self._pout, self.ed.number, res) # Шаг 4: Переход в состояние CONVERTED self._state = State.CONVERTED return res # Возвращаем результат для отображения else: # Любая другая команда (ввод, удаление, очистка) # Переход в состояние EDITING self._state = State.EDITING # Делегирование обработки редактору return self.ed.do_edit(j) ``` ##### `acc() -> int` Расчёт необходимой точности (количества знаков после запятой) для результата. **Формула из ТЗ:** ``` n₂ = n₁ × ln(p₁) / ln(p₂) + 0.5 ``` Где: - `n₁` — количество знаков после запятой в исходном числе - `n₂` — необходимое количество знаков после запятой в результате - `p₁` — исходное основание - `p₂` — целевое основание **Реализация:** ```python def acc(self) -> int: # Если нет дробной части — точность 0 if self.ed.acc() == 0: return 0 # Расчёт по формуле val = self.ed.acc() * math.log(self._pin) / math.log(self._pout) + 0.5 # Округление до ближайшего целого return int(round(val)) ``` **Пример расчёта:** - Исходное: `0.110` (p₁=2, n₁=3 знака) - Целевое: p₂=10 - Расчёт: `n₂ = 3 × ln(2) / ln(10) + 0.5 = 3 × 0.301 + 0.5 = 1.403 ≈ 1` - Результат будет показан с 1 знаком после запятой --- ### 3. Editor.py — Редактор Чисел **Ответственность:** Инкапсуляция логики формирования строки числа, обработка операций редактирования. #### Класс `Editor` **Атрибуты:** | Атрибут | Тип | Описание | Значение по умолчанию | |---------|-----|----------|----------------------| | `_number` | `str` | Текущая строка числа | `"0"` | | `_delim` | `str` | Символ десятичного разделителя | `"."` | | `_zero` | `str` | Символ нуля | `"0"` | **Методы:** ##### `add_digit(n: int) -> str` Добавляет цифру к текущему числу. **Логика:** ```python def add_digit(self, n: int) -> str: digit_char = self._int_to_char(n) # Если текущее число "0", заменяем его на новую цифру if self._number == self._zero: self._number = digit_char else: # Иначе дописываем цифру в конец self._number += digit_char return self._number ``` **Примеры:** - `add_digit(5)` при `_number="0"` → `"5"` - `add_digit(3)` при `_number="5"` → `"53"` - `add_digit(10)` при `_number="53"` → `"53A"` ##### `add_zero() -> str` Добавляет ноль к числу (не добавляет, если число уже "0"). ##### `add_delim() -> str` Добавляет десятичный разделитель, если его ещё нет. **Логика:** ```python def add_delim(self) -> str: if self._delim not in self._number: self._number += self._delim return self._number ``` **Примеры:** - `add_delim()` при `_number="123"` → `"123."` - `add_delim()` при `_number="123.45"` → `"123.45"` (без изменений) ##### `bs() -> str` Удаляет последний символ (Backspace). **Логика:** ```python def bs(self) -> str: if len(self._number) > 1: self._number = self._number[:-1] # Удаляем последний символ else: self._number = self._zero # Оставляем "0" если это последний символ return self._number ``` **Примеры:** - `bs()` при `_number="123.45"` → `"123.4"` - `bs()` при `_number="5"` → `"0"` ##### `clear() -> str` Полностью очищает число, устанавливая в "0". ##### `acc() -> int` Возвращает количество знаков после десятичного разделителя. **Логика:** ```python def acc(self) -> int: if self._delim in self._number: # Находим позицию разделителя и считаем знаки после него return len(self._number) - self._number.find(self._delim) - 1 return 0 # Нет разделителя — нет дробной части ``` **Примеры:** - `acc()` при `_number="123.456"` → `3` - `acc()` при `_number="123"` → `0` ##### `do_edit(j: int) -> str` Маршрутизатор команд редактирования. | Код `j` | Действие | |---------|----------| | 0-15 | `add_digit(j)` — ввод цифры | | 16 | `add_delim()` — добавление разделителя | | 17 | `bs()` — удаление символа | | 18 | `clear()` — полная очистка | ##### `_int_to_char(n: int) -> str` (приватный) Преобразует целое число 0-15 в символ цифры. ```python def _int_to_char(self, n: int) -> str: if 0 <= n <= 9: return str(n) # 0-9 → "0"-"9" elif 10 <= n <= 15: return chr(ord("A") + n - 10) # 10-15 → "A"-"F" return "?" # Невалидное значение ``` --- ### 4. Conver_10_p.py — Конвертация: Десятичная → p-ичная **Ответственность:** Перевод числа из десятичной системы счисления в систему с основанием p (2 ≤ p ≤ 16). #### Класс `Conver_10_p` (статический) **Методы:** ##### `int_to_Char(d: int) -> str` Преобразует целое число 0-15 в символ цифры. **Таблица преобразования:** | d | Возвращаемое значение | |---|----------------------| | 0-9 | "0"-"9" | | 10 | "A" | | 11 | "B" | | 12 | "C" | | 13 | "D" | | 14 | "E" | | 15 | "F" | ##### `int_to_P(n: int, p: int) -> str` Переводит **целую часть** числа из десятичной в p-ичную систему. **Алгоритм (метод последовательного деления):** ```python def int_to_P(n: int, p: int) -> str: if n == 0: return "0" res = "" while n > 0: # Берём остаток от деления — это следующая цифра res = Conver_10_p.int_to_Char(n % p) + res # Делим на основание n //= p return res ``` **Пример: 25 → двоичная (p=2)** ``` 25 ÷ 2 = 12, остаток 1 → res = "1" 12 ÷ 2 = 6, остаток 0 → res = "01" 6 ÷ 2 = 3, остаток 0 → res = "001" 3 ÷ 2 = 1, остаток 1 → res = "1001" 1 ÷ 2 = 0, остаток 1 → res = "11001" Ответ: "11001" ``` ##### `flt_to_P(n: float, p: int, c: int) -> str` Переводит **дробную часть** числа из десятичной в p-ичную систему. **Алгоритм (метод последовательного умножения):** ```python def flt_to_P(n: float, p: int, c: int) -> str: res = "" for _ in range(c): # c — количество знаков после запятой n *= p # Умножаем дробь на основание digit = int(n) # Целая часть — следующая цифра res += Conver_10_p.int_to_Char(digit) n -= digit # Оставляем только дробную часть return res ``` **Пример: 0.625 → двоичная (p=2, c=3)** ``` Итерация 1: 0.625 × 2 = 1.25 → digit=1, res="1", n=0.25 Итерация 2: 0.25 × 2 = 0.5 → digit=0, res="10", n=0.5 Итерация 3: 0.5 × 2 = 1.0 → digit=1, res="101", n=0.0 Ответ: "101" ``` ##### `Do(n: float, p: int, c: int) -> str` Основной метод конвертации. Обрабатывает знак, целую и дробную части. **Логика:** ```python def Do(n: float, p: int, c: int) -> str: # 1. Обработка знака sign = "-" if n < 0 else "" n = abs(n) # 2. Разделение на целую и дробную части int_part = int(n) flt_part = n - int_part # 3. Конвертация целой части res = Conver_10_p.int_to_P(int_part, p) # 4. Конвертация дробной части (если требуется) if c > 0: res += "." + Conver_10_p.flt_to_P(flt_part, p, c) return sign + res ``` **Пример полного преобразования:** - Вход: `n=25.625, p=2, c=3` - Целая часть: `25 → "11001"` - Дробная часть: `0.625 → "101"` - Результат: `"11001.101"` --- ### 5. Conver_p_10.py — Конвертация: p-ичная → Десятичная **Ответственность:** Перевод числа из p-ичной системы счисления (2 ≤ p ≤ 16) в десятичную. #### Класс `Conver_p_10` (статический) **Методы:** ##### `char_To_num(ch: str) -> int` Преобразует символ цифры в целое число. **Таблица преобразования:** | ch | Возвращаемое значение | |----|----------------------| | "0"-"9" | 0-9 | | "A"-"F" (регистронезависимо) | 10-15 | ##### `convert(P_num: str, P: int, weight: float) -> float` Вспомогательный метод для вычисления значения числа по разрядам. **Алгоритм:** ```python def convert(P_num: str, P: int, weight: float) -> float: res = 0.0 for char in P_num: # Добавляем вклад текущего разряда res += Conver_p_10.char_To_num(char) * weight # Уменьшаем вес для следующего разряда weight /= P return res ``` **Принцип работы:** Каждый разряд числа имеет «вес» — основание в степени позиции. Метод проходит по всем цифрам слева направо, умножая цифру на её вес и суммируя результаты. ##### `dval(P_num: str, P: int) -> float` Основной метод перевода из p-ичной в десятичную систему. **Логика:** ```python def dval(P_num: str, P: int) -> float: # 1. Обработка пустой строки if not P_num: return 0.0 # 2. Обработка знака sign = 1.0 if P_num.startswith("-"): sign = -1.0 P_num = P_num[1:] # 3. Разделение на целую и дробную части if "." in P_num: parts = P_num.split(".") int_part = parts[0] flt_part = parts[1] full_str = int_part + flt_part # Объединяем для обработки # Вес старшего разряда целой части weight = P ** (len(int_part) - 1) else: full_str = P_num weight = P ** (len(P_num) - 1) # 4. Вычисление значения return sign * Conver_p_10.convert(full_str, P, float(weight)) ``` **Пример: "11001.101" (p=2) → десятичная** 1. Разделение: `int_part="11001"`, `flt_part="101"` 2. Объединение: `full_str="11001101"` 3. Начальный вес: `2^(5-1) = 2^4 = 16` 4. Вычисление: ``` 1 × 16 = 16.0 1 × 8 = 8.0 0 × 4 = 0.0 0 × 2 = 0.0 1 × 1 = 1.0 1 × 0.5 = 0.5 0 × 0.25 = 0.0 1 × 0.125= 0.125 ───────────────── Сумма = 25.625 ``` 5. Результат: `25.625` --- ### 6. History.py — Журнал Операций **Ответственность:** Хранение и предоставление истории всех выполненных конвертаций. #### Класс `Record` Представляет одну запись в журнале. **Атрибуты:** | Атрибут | Тип | Описание | |---------|-----|----------| | `p1` | `int` | Исходное основание | | `p2` | `int` | Целевое основание | | `number1` | `str` | Исходное число | | `number2` | `str` | Результат конвертации | **Метод `__str__()`:** Возвращает человекочитаемое представление записи: ```python def __str__(self) -> str: return f"{self.number1} ({self.p1}) -> {self.number2} ({self.p2})" ``` **Пример вывода:** `"1011.1 (2) -> B.8 (16)"` #### Класс `History` Управляет коллекцией записей. **Атрибуты:** | Атрибут | Тип | Описание | |---------|-----|----------| | `_list` | `list[Record]` | Внутренний список записей | **Методы:** | Метод | Описание | |-------|----------| | `add_record(p1, p2, n1, n2)` | Добавляет новую запись в журнал | | `clear()` | Очищает весь журнал | | `count() -> int` | Возвращает количество записей | | `get_record(i) -> Record` | Возвращает запись по индексу (0-based) | --- ## 🔢 Алгоритмы Конвертации ### Общий Принцип Конвертация между произвольными системами счисления выполняется в **два этапа**: ``` p₁-ичное число ──────→ Десятичное ──────→ p₂-ичное число (промежуточное) ``` **Почему через десятичную?** - Десятичная система является «универсальным посредником» - Упрощает реализацию (нужно реализовать только 2 направления вместо n²) - Легче отладка и тестирование ### Алгоритм: p₁ → 10 → p₂ ``` ┌─────────────────────────────────────────────────────────────┐ │ Шаг 1: Чтение исходного числа │ │ - Вход: строка "1A3.F" (p₁=16) │ │ - Проверка знака │ │ - Разделение на целую и дробную части │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Шаг 2: Конвертация в десятичную (Conver_p_10.dval) │ │ - Вычисление веса старшего разряда: p₁^(len(int)-1) │ │ - Последовательное суммирование: digit × weight │ │ - Деление веса на p₁ для каждого следующего разряда │ │ - Выход: float 419.9375 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Шаг 3: Расчёт точности (Control_.acc) │ │ - Подсчёт знаков после запятой в исходном числе: n₁ │ │ - Формула: n₂ = n₁ × ln(p₁) / ln(p₂) + 0.5 │ │ - Выход: n₂ = 1 × ln(16) / ln(10) + 0.5 ≈ 1.7 ≈ 2 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Шаг 4: Конвертация в p₂ (Conver_10_p.Do) │ │ - Целая часть: метод последовательного деления на p₂ │ │ - Дробная часть: метод последовательного умножения на p₂ │ │ - Выход: строка "1A3.F0" (p₂=16) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 🔄 Машина Состояний Приложение реализует простую детерминированную машину состояний (Finite State Machine). ### Диаграмма Состояний ``` ┌──────────────────┐ │ │ ▼ │ ┌─────────┐ CMD 19 (EXEC) │ │ EDITING │ ──────────────────► ┌─────────────┐ │ │ │ CONVERTED │ │ (1) │ │ (2) │ └─────────┘ └─────────────┘ ▲ │ │ │ └─────────────────────────────────┘ CMD 0-18 (любая кроме EXEC) ``` ### Таблица Переходов | Текущее состояние | Команда | Новое состояние | Действие | |-------------------|---------|-----------------|----------| | EDITING | 0-18 | EDITING | Редактирование числа | | EDITING | 19 (EXEC) | CONVERTED | Выполнение конвертации | | CONVERTED | 0-18 | EDITING | Начало нового ввода | | CONVERTED | 19 (EXEC) | CONVERTED | Повторная конвертация того же числа | ### Реализация в Control_.do_cmnd ```python def do_cmnd(self, j: int) -> str: if j == 19: # Переход: EDITING → CONVERTED (или оставаться в CONVERTED) self._state = State.CONVERTED # ... логика конвертации ... return res else: # Переход: CONVERTED → EDITING (или оставаться в EDITING) self._state = State.EDITING return self.ed.do_edit(j) ``` ### Влияние на GUI Состояние влияет на поведение интерфейса: - **В состоянии EDITING:** - `label_result` показывает "0" (плейсхолдер) - Активен ввод цифр - **В состоянии CONVERTED:** - `label_result` показывает результат конвертации - Следующий ввод цифры автоматически начинает новый сеанс --- ## 🎯 Расчёт Точности ### Математическое Обоснование При переводе дробной части числа из системы с основанием p₁ в систему с основанием p₂ может потребоваться разное количество знаков для сохранения эквивалентной точности. **Формула:** ``` n₂ = n₁ × logₚ₂(p₁) + 0.5 = n₁ × ln(p₁) / ln(p₂) + 0.5 ``` Где: - `n₁` — количество знаков после запятой в исходном числе - `n₂` — необходимое количество знаков в результате - `+ 0.5` — для корректного округления до ближайшего целого ### Примеры Расчёта #### Пример 1: Двоичная → Десятичная - Исходное: `0.1101` (p₁=2, n₁=4) - Целевое: p₂=10 - Расчёт: `n₂ = 4 × ln(2) / ln(10) + 0.5 = 4 × 0.301 + 0.5 = 1.704 ≈ 2` - Результат будет показан с **2 знаками** после запятой #### Пример 2: Десятичная → Двоичная - Исходное: `0.25` (p₁=10, n₁=2) - Целевое: p₂=2 - Расчёт: `n₂ = 2 × ln(10) / ln(2) + 0.5 = 2 × 3.322 + 0.5 = 7.144 ≈ 7` - Результат будет показан с **7 знаками** после запятой #### Пример 3: Без дробной части - Исходное: `123` (n₁=0) - Любое целевое p₂ - Расчёт: `n₂ = 0` (специальная проверка в коде) - Дробная часть в результате не выводится ### Реализация в Коде ```python def acc(self) -> int: # Специальный случай: нет дробной части if self.ed.acc() == 0: return 0 # Применении формулы val = self.ed.acc() * math.log(self._pin) / math.log(self._pout) + 0.5 # Округление до ближайшего целого return int(round(val)) ``` --- ## ⌨️ Обработка Ввода ### Поддерживаемые Способы Ввода #### 1. Кнопки на Экране Сетка кнопок 5×4 в `MainForm`: ``` ┌───┬───┬───┬───┐ │ 0 │ 1 │ 2 │ 3 │ ├───┼───┼───┼───┤ │ 4 │ 5 │ 6 │ 7 │ ├───┼───┼───┼───┤ │ 8 │ 9 │ A │ B │ ├───┼───┼───┼───┤ │ C │ D │ E │ F │ ├───┼───┼───┼───┤ │ . │BS │CL │EXE│ └───┴───┴───┴───┘ ``` **Динамическая блокировка:** Метод `update_buttons()` блокирует кнопки с цифрами ≥ p₁: ```python def update_buttons(self): for i in range(16): self.buttons[i].setEnabled(i < self.ctl.pin) ``` **Примеры:** - p₁=2: активны только кнопки "0", "1" - p₁=10: активны кнопки "0"-"9" - p₁=16: активны все кнопки "0"-"F" #### 2. Клавиатура Обработчик `keyPressEvent` поддерживает: | Клавиша | Действие | |---------|----------| | 0-9 | Ввод соответствующей цифры | | A-F | Ввод букв (для p₁ > 10) | | . или , | Добавление десятичного разделителя | | Backspace | Удаление последнего символа (BS) | | Delete | Полная очистка (CL) | | Enter | Выполнение конвертации (EXEC) | **Проверка допустимости:** ```python if "0" <= key <= "9": val = ord(key) - ord("0") if val < self.ctl.pin: # Проверка: цифра < p₁ self.do_command(val) ``` #### 3. Спинбоксы и Слайдеры **Двусторонняя синхронизация:** ```python # Спинбокс → Слайдер def p1_changed(self, val): self.slider_p1.setValue(val) self.ctl.pin = val self.update_buttons() self.do_command(18) # Сброс при смене основания # Слайдер → Спинбокс def p1_slider_changed(self, val): self.spin_p1.setValue(val) ``` --- ## 📚 Журнал Операций ### Структура Записи Каждая запись содержит: - `p1` — исходное основание - `p2` — целевое основание - `number1` — исходное число (строка) - `number2` — результат (строка) ### Добавление в Журнал Происходит автоматически при выполнении конвертации (CMD 19): ```python # В Control_.do_cmnd: if j == 19: # ... конвертация ... self.his.add_record(self._pin, self._pout, self.ed.number, res) ``` ### Просмотр Журнала 1. Меню «Справка» → «История» 2. Открывается `HistoryForm` с `QListWidget` 3. Отображаются все записи в формате: `"число (p1) -> результат (p2)"` ### Очистка Журнала Журнал очищается при перезапуске приложения. Метод `clear()` существует, но не вызывается из GUI (можно добавить через контекстное меню). --- ## 🎨 Стилизация UI ### Цветовая Схема Приложение использует цветовую схему **Catppuccin Mocha**: | Цвет | Hex | Использование | |------|-----|---------------| | Base (фон) | `#1e1e2e` | Основное окно | | Mantle (тёмный фон) | `#11111b` | Поля ввода, меню | | Surface | `#313244` | Кнопки, спинбоксы | | Overlay1 | `#45475a` | Границы, hover-эффекты | | Blue | `#89b4fa` | Акцентные кнопки, слайдеры | | Green | `#a6e3a1` | Результат конвертации | | Red | `#f38ba8` | Кнопки очистки | | Text | `#cdd6f4` | Основной текст | ### CSS-подобные Стили Стили применяются через `setStyleSheet()` с использованием подмножества CSS: ```python self.setStyleSheet(""" QMainWindow { background-color: #1e1e2e; } QPushButton { background-color: #313244; border: none; border-radius: 8px; padding: 10px; font-size: 12pt; } QPushButton:hover { background-color: #45475a; } QPushButton:pressed { background-color: #585b70; } QPushButton#ActionBtn { background-color: #89b4fa; color: #1e1e2e; } QPushButton#ClearBtn { background-color: #f38ba8; color: #1e1e2e; } QLabel#DisplayLabel { background-color: #11111b; border: 2px solid #45475a; border-radius: 10px; padding: 15px; font-size: 24pt; font-weight: bold; color: #89b4fa; } """) ``` ### Стилизованные Элементы | Элемент | Идентификатор | Стиль | |---------|--------------|-------| | Поле исходного числа | `DisplayLabel` | Тёмный фон, крупный шрифт, синяя обводка | | Поле результата | `ResultLabel` | Зелёный текст (`#a6e3a1`) | | Кнопка EXEC | `ActionBtn` | Синяя (`#89b4fa`) | | Кнопки BS/CL | `ClearBtn` | Красная (`#f38ba8`) | --- ## 📊 Таблица Соответствия ТЗ | Требование из task.pdf | Реализация в коде | Файл(ы) | |------------------------|-------------------|---------| | **Диапазон оснований p₁, p₂ (2..16)** | `QSpinBox.setRange(2, 16)`, проверка `i < self.ctl.pin` | `main.py` | | **Перевод действительных чисел** | Поддержка `float` в `Conver_p_10.dval` и `Conver_10_p.Do` | `Conver_p_10.py`, `Conver_10_p.py` | | **Ввод с помощью экранных кнопок** | Сетка `QGridLayout` с `QPushButton`, `do_command()` | `main.py` | | **Ввод с помощью клавиатуры** | Переопределённый `keyPressEvent()` | `main.py` | | **История (журнал) сеанса** | Классы `Record` и `History`, `HistoryForm` | `History.py`, `main.py` | | **Справка о приложении** | Меню «Справка» → `AboutForm` | `main.py` | | **Автоматический расчёт точности** | Метод `acc()` с формулой `n₂ = n₁ × ln(p₁)/ln(p₂) + 0.5` | `Control_.py` | | **Состояния «Редактирование» / «Преобразовано»** | `enum State`, свойство `state` | `Control_.py` | | **Удаление символа справа (BS)** | Метод `Editor.bs()`, команда 17 | `Editor.py`, `Control_.py` | | **Очистка числа (CE/CL)** | Метод `Editor.clear()`, команда 18 | `Editor.py`, `Control_.py` | | **Динамическая блокировка кнопок** | Метод `update_buttons()` | `main.py` | | **Синхронизация слайдеров и спинбоксов** | Двусторонние коннекты `valueChanged` | `main.py` | | **Поддержка точки и запятой** | Обработка обоих символов в `keyPressEvent` | `main.py` | | **Обработка отрицательных чисел** | Проверка знака в `Conver_p_10.dval` и `Conver_10_p.Do` | Оба конвертера | --- ## 🔧 Расширение и Модификация ### Добавление Новых Оснований (>16) Для поддержки оснований больше 16: 1. Обновить диапазоны в `QSpinBox` и `QSlider`: ```python self.spin_p1.setRange(2, 36) # Вместо (2, 16) ``` 2. Расширить `_int_to_char` и `char_To_num`: ```python def _int_to_char(n: int) -> str: if 0 <= n <= 9: return str(n) elif 10 <= n <= 35: return chr(ord("A") + n - 10) # До "Z" ``` 3. Обновить сетку кнопок (добавить G-Z) ### Добавление Экспорта Истории ```python def export_to_file(self, filename: str): with open(filename, 'w', encoding='utf-8') as f: for i in range(self.count()): f.write(str(self.get_record(i)) + '\n') ``` ### Добавление Тёмной/Светлой Темы Создать словарь тем и переключать через метод: ```python themes = { "dark": { "bg": "#1e1e2e", "text": "#cdd6f4", ... }, "light": { "bg": "#eff1f5", "text": "#4c4f69", ... } } ``` --- ## 🧪 Тестирование ### Примеры Тестовых Сценариев | № | Исходное | p₁ | p₂ | Ожидаемый результат | |---|----------|----|----|---------------------| | 1 | 1011.1 | 2 | 10 | 11.5 | | 2 | 25.625 | 10 | 2 | 11001.101 | | 3 | 1A3.F | 16 | 10 | 419.9375 | | 4 | -15 | 10 | 2 | -1111 | | 5 | 0.1 | 2 | 10 | 0.5 | ### Проверка Граничных Случаев 1. **Минимальное основание:** p₁=2, p₂=2 (конвертация в ту же систему) 2. **Максимальное основание:** p₁=16, p₂=16 3. **Ноль:** ввод "0" → конвертация → "0" 4. **Отрицательные числа:** "-FF" (16) → "-255" (10) 5. **Длинная дробная часть:** проверка точности расчёта --- ## 📝 Заключение Данный проект демонстрирует: - **Объектно-ориентированный подход** с чётким разделением ответственности - **Паттерн MVC** (Model-View-Controller) в упрощённой форме - **Машину состояний** для управления потоком приложения - **Математически корректные алгоритмы** конвертации систем счисления - **Современный UI** с использованием PyQt6 и кастомной стилизации Архитектура приложения позволяет легко расширять функциональность (новые системы счисления, экспорт данных, дополнительные темы) без изменения существующего кода.