|
|
@@ -0,0 +1,1168 @@
|
|
|
+import tkinter as tk
|
|
|
+from tkinter import messagebox
|
|
|
+import random
|
|
|
+import os
|
|
|
+from pathlib import Path
|
|
|
+from typing import Dict, List, Optional, Tuple
|
|
|
+
|
|
|
+# Попытка импорта PIL для работы с изображениями
|
|
|
+try:
|
|
|
+ from PIL import Image
|
|
|
+ PIL_AVAILABLE = True
|
|
|
+except ImportError:
|
|
|
+ PIL_AVAILABLE = False
|
|
|
+ print("PIL (Pillow) не установлен. Установите: pip install Pillow")
|
|
|
+
|
|
|
+# ==================== КОНСТАНТЫ ====================
|
|
|
+WINDOW_SIZE = "1000x700"
|
|
|
+IMAGES_FOLDER = "quiz_images"
|
|
|
+SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.ico', '.tiff')
|
|
|
+DEFAULT_COLORS = [
|
|
|
+ '#f0f8ff', '#f5f5dc', '#e6e6fa', '#ffe4e1', '#f0fff0',
|
|
|
+ '#fff0f5', '#e0ffff', '#faf0e6', '#fffacd', '#f8f8ff'
|
|
|
+]
|
|
|
+
|
|
|
+# ==================== СПИСОК ВОПРОСОВ ====================
|
|
|
+questions = [
|
|
|
+ {
|
|
|
+ 'id': 1,
|
|
|
+ 'question': 'Что понимается под "персональными данными" согласно законодательству?',
|
|
|
+ 'options': [
|
|
|
+ 'Только паспортные данные и фотографии',
|
|
|
+ 'Любая информация, прямо или косвенно относящаяся к конкретному физическому лицу',
|
|
|
+ 'Только контактные данные человека',
|
|
|
+ 'Информация о юридических лицах'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Персональные данные - любые сведения, которые относятся, непосредственно либо косвенно, к конкретному физическому лицу.',
|
|
|
+ 'image': 'q1.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 2,
|
|
|
+ 'question': 'Кто такой "оператор" персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Человек, чьи данные обрабатываются',
|
|
|
+ 'Государственный орган, контролирующий защиту данных',
|
|
|
+ 'Лицо или орган, осуществляющий обработку ПД и определяющий цели обработки',
|
|
|
+ 'Сотрудник, работающий с базами данных'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'Оператор - лицо или орган, осуществляющий обработку персональных данных и определяющий цели обработки, состав данных и действия с ними.',
|
|
|
+ 'image': 'q2.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 3,
|
|
|
+ 'question': 'Что включает в себя система защиты персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Только антивирусное программное обеспечение',
|
|
|
+ 'Только организационные меры',
|
|
|
+ 'Организационные и технические меры, определенные с учетом актуальных угроз',
|
|
|
+ 'Только физическую охрану помещений'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'Система защиты включает организационные и технические меры, определенные с учетом актуальных угроз безопасности.',
|
|
|
+ 'image': 'q3.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 5,
|
|
|
+ 'question': 'Сколько уровней защищенности персональных данных устанавливается при их обработке в информационных системах?',
|
|
|
+ 'options': [
|
|
|
+ '2 уровня',
|
|
|
+ '3 уровня',
|
|
|
+ '4 уровня',
|
|
|
+ '5 уровней'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'При обработке персональных данных в информационных системах устанавливаются 4 уровня защищенности.',
|
|
|
+ 'image': 'q5.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 6,
|
|
|
+ 'question': 'Какие данные относятся к специальным категориям персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'ФИО и адрес проживания',
|
|
|
+ 'Информация о расовой принадлежности, политических взглядах, здоровье',
|
|
|
+ 'Номер телефона и email',
|
|
|
+ 'Место работы и должность'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'К специальным категориям относятся данные о расовой принадлежности, политических взглядах, религиозных убеждениях, здоровье.',
|
|
|
+ 'image': 'q6.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 7,
|
|
|
+ 'question': 'Что относится к биометрическим персональным данным?',
|
|
|
+ 'options': [
|
|
|
+ 'ФИО и дата рождения',
|
|
|
+ 'Физиологические особенности для идентификации: отпечатки пальцев, ДНК, группа крови',
|
|
|
+ 'Номер паспорта',
|
|
|
+ 'Адрес проживания'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Биометрические данные - физиологические особенности, используемые для идентификации личности.',
|
|
|
+ 'image': 'q7.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 8,
|
|
|
+ 'question': 'Как часто должен проводиться контроль за выполнением требований защиты ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Ежемесячно',
|
|
|
+ 'Ежегодно',
|
|
|
+ 'Не реже 1 раза в 3 года',
|
|
|
+ 'Один раз в 5 лет'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'Контроль проводится не реже 1 раза в 3 года в сроки, определяемые оператором.',
|
|
|
+ 'image': 'q8.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 9,
|
|
|
+ 'question': 'Какой федеральный закон регулирует защиту персональных данных в России?',
|
|
|
+ 'options': [
|
|
|
+ 'ФЗ "Об информации"',
|
|
|
+ 'Федеральный закон №152-ФЗ "О персональных данных"',
|
|
|
+ 'ФЗ "О связи"',
|
|
|
+ 'ФЗ "О безопасности"'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'В России требования к обработке и защите персональных данных определяет Федеральный закон №152-ФЗ "О персональных данных".',
|
|
|
+ 'image': 'q9.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 10,
|
|
|
+ 'question': 'Что такое GDPR?',
|
|
|
+ 'options': [
|
|
|
+ 'Российский закон о данных',
|
|
|
+ 'Американский стандарт безопасности',
|
|
|
+ 'Европейский регламент по защите данных',
|
|
|
+ 'Международный стандарт шифрования'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'GDPR - Общий регламент по защите данных, служит правилом для защиты личных данных граждан ЕС.',
|
|
|
+ 'image': 'q10.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 11,
|
|
|
+ 'question': 'Какие требования предъявляются к сбору персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Сбор только с согласия субъекта и ограничение только необходимыми данными',
|
|
|
+ 'Сбор любой доступной информации',
|
|
|
+ 'Сбор без согласия, если это удобно',
|
|
|
+ 'Только сбор через официальные запросы'
|
|
|
+ ],
|
|
|
+ 'correct': 0,
|
|
|
+ 'explanation': 'Персональные данные могут собираться только при согласии субъекта и ограничиваться только необходимыми данными.',
|
|
|
+ 'image': 'q11.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 12,
|
|
|
+ 'question': 'Что означает требование "целостность и конфиденциальность" ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Данные должны быть красиво оформлены',
|
|
|
+ 'Защита от несанкционированного доступа, потери и уничтожения',
|
|
|
+ 'Данные должны быть доступны всем',
|
|
|
+ 'Данные должны храниться вечно'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Данные должны защищаться от несанкционированного доступа, потери, повреждения или уничтожения.',
|
|
|
+ 'image': 'q12.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 13,
|
|
|
+ 'question': 'Что означает "ограничение сроков хранения" персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Хранить данные бесконечно',
|
|
|
+ 'Хранить только необходимое для целей обработки время',
|
|
|
+ 'Удалять сразу после получения',
|
|
|
+ 'Хранить минимум 10 лет'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Хранить персональные сведения можно только столько, сколько необходимо для достижения целей их обработки.',
|
|
|
+ 'image': 'q13.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 14,
|
|
|
+ 'question': 'В каких сферах применяются персональные данные?',
|
|
|
+ 'options': [
|
|
|
+ 'Только в медицине',
|
|
|
+ 'Только в банковской сфере',
|
|
|
+ 'Практически во всех сферах: коммерция, финансы, медицина, образование, госорганы',
|
|
|
+ 'Только в интернете'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'Персональные данные используются практически в любой сфере современного цифрового мира.',
|
|
|
+ 'image': 'q14.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 15,
|
|
|
+ 'question': 'Какие данные относятся к "общим" согласно 152-ФЗ?',
|
|
|
+ 'options': [
|
|
|
+ 'ФИО, место регистрации, образование, контактная информация',
|
|
|
+ 'Данные о здоровье',
|
|
|
+ 'Отпечатки пальцев',
|
|
|
+ 'Политические взгляды'
|
|
|
+ ],
|
|
|
+ 'correct': 0,
|
|
|
+ 'explanation': 'Общие данные: ФИО, место регистрации, сведения об образовании, месте работы, контактная информация.',
|
|
|
+ 'image': 'q15.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 16,
|
|
|
+ 'question': 'Что должно быть предусмотрено в договоре между оператором и уполномоченным лицом?',
|
|
|
+ 'options': [
|
|
|
+ 'Стоимость услуг',
|
|
|
+ 'Обязанность обеспечить безопасность ПД при обработке',
|
|
|
+ 'Срок действия договора',
|
|
|
+ 'Реквизиты сторон'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Договор должен предусматривать обязанность уполномоченного лица обеспечить безопасность ПД при их обработке.',
|
|
|
+ 'image': 'q16.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 17,
|
|
|
+ 'question': 'Кто обеспечивает безопасность персональных данных в информационной системе?',
|
|
|
+ 'options': [
|
|
|
+ 'ФСБ России',
|
|
|
+ 'Оператор системы или уполномоченное им лицо',
|
|
|
+ 'Роскомнадзор',
|
|
|
+ 'Сам субъект данных'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Безопасность обеспечивает оператор системы или лицо, осуществляющее обработку по поручению оператора.',
|
|
|
+ 'image': 'q17.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 18,
|
|
|
+ 'question': 'Какие угрозы относятся к 1-му типу?',
|
|
|
+ 'options': [
|
|
|
+ 'Только внешние атаки',
|
|
|
+ 'Угрозы, связанные с недокументированными возможностями в системном ПО',
|
|
|
+ 'Только физические угрозы',
|
|
|
+ 'Угрозы от сотрудников'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Угрозы 1-го типа связаны с наличием недокументированных возможностей в системном программном обеспечении.',
|
|
|
+ 'image': 'q18.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 19,
|
|
|
+ 'question': 'Какие угрозы относятся ко 2-му типу?',
|
|
|
+ 'options': [
|
|
|
+ 'Угрозы от хакеров',
|
|
|
+ 'Угрозы, связанные с недокументированными возможностями в прикладном ПО',
|
|
|
+ 'Угрозы пожаров и наводнений',
|
|
|
+ 'Угрозы отключения электричества'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Угрозы 2-го типа связаны с наличием недокументированных возможностей в прикладном программном обеспечении.',
|
|
|
+ 'image': 'q19.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 20,
|
|
|
+ 'question': 'Какие угрозы относятся к 3-му типу?',
|
|
|
+ 'options': [
|
|
|
+ 'Угрозы из интернета',
|
|
|
+ 'Угрозы, не связанные с недокументированными возможностями в системном и прикладном ПО',
|
|
|
+ 'Только физические угрозы',
|
|
|
+ 'Угрозы от вирусов'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Угрозы 3-го типа - угрозы, не связанные с наличием недокументированных возможностей в ПО.',
|
|
|
+ 'image': 'q20.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 21,
|
|
|
+ 'question': 'Когда устанавливается необходимость 1-го уровня защищенности?',
|
|
|
+ 'options': [
|
|
|
+ 'Всегда для всех систем',
|
|
|
+ 'При угрозах 1 типа и обработке специальных или биометрических данных',
|
|
|
+ 'Только для государственных систем',
|
|
|
+ 'При обработке менее 100 записей'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': '1-й уровень необходим при угрозах 1 типа и обработке специальных категорий или биометрических данных.',
|
|
|
+ 'image': 'q21.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 22,
|
|
|
+ 'question': 'Какие требования предъявляются для 4-го уровня защищенности?',
|
|
|
+ 'options': [
|
|
|
+ 'Создание специального отдела',
|
|
|
+ 'Организация безопасности помещений, сохранность носителей, утверждение списка допущенных лиц',
|
|
|
+ 'Шифрование всех данных',
|
|
|
+ 'Ежедневный аудит'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': '4-й уровень требует безопасности помещений, сохранности носителей и утверждения списка допущенных лиц.',
|
|
|
+ 'image': 'q22.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 23,
|
|
|
+ 'question': 'Что дополнительно требуется для 3-го уровня защищенности?',
|
|
|
+ 'options': [
|
|
|
+ 'Создание отдела защиты данных',
|
|
|
+ 'Назначение ответственного за безопасность ПД в ИС',
|
|
|
+ 'Автоматическая регистрация изменений',
|
|
|
+ 'Шифрование каналов связи'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Для 3-го уровня необходимо назначение должностного лица, ответственного за безопасность ПД в ИС.',
|
|
|
+ 'image': 'q23.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 24,
|
|
|
+ 'question': 'Что дополнительно требуется для 2-го уровня защищенности?',
|
|
|
+ 'options': [
|
|
|
+ 'Видеонаблюдение',
|
|
|
+ 'Ограничение доступа к электронному журналу сообщений',
|
|
|
+ 'Биометрическая идентификация',
|
|
|
+ 'Еженедельные проверки'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Для 2-го уровня доступ к электронному журналу должен быть только для лиц, которым это необходимо.',
|
|
|
+ 'image': 'q24.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 25,
|
|
|
+ 'question': 'Что дополнительно требуется для 1-го уровня защищенности?',
|
|
|
+ 'options': [
|
|
|
+ 'Автоматическая регистрация изменений полномочий и создание структурного подразделения',
|
|
|
+ 'Ежечасный мониторинг',
|
|
|
+ 'Вооруженная охрана',
|
|
|
+ 'Спутниковое наблюдение'
|
|
|
+ ],
|
|
|
+ 'correct': 0,
|
|
|
+ 'explanation': '1-й уровень требует автоматической регистрации изменений полномочий и создания структурного подразделения.',
|
|
|
+ 'image': 'q25.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 26,
|
|
|
+ 'question': 'Что должно содержать уведомление в Роскомнадзор о начале обработки ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Только название организации',
|
|
|
+ 'Информацию о целях обработки, категориях данных и мерах защиты',
|
|
|
+ 'Только контактные данные',
|
|
|
+ 'Только ФИО директора'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Уведомление должно содержать информацию о целях, категориях данных и мерах защиты.',
|
|
|
+ 'image': 'q26.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 27,
|
|
|
+ 'question': 'Что такое трансграничная передача данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Передача внутри организации',
|
|
|
+ 'Передача данных за пределы государства',
|
|
|
+ 'Передача по локальной сети',
|
|
|
+ 'Передача на флешке'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Трансграничная передача - это передача персональных данных на территорию иностранного государства.',
|
|
|
+ 'image': 'q27.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 28,
|
|
|
+ 'question': 'Кто должен давать согласие на обработку данных детей?',
|
|
|
+ 'options': [
|
|
|
+ 'Сам ребенок',
|
|
|
+ 'Учитель',
|
|
|
+ 'Законные представители (родители)',
|
|
|
+ 'Директор школы'
|
|
|
+ ],
|
|
|
+ 'correct': 2,
|
|
|
+ 'explanation': 'Для обработки данных детей требуется согласие законных представителей.',
|
|
|
+ 'image': 'q28.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 29,
|
|
|
+ 'question': 'Что должно быть сделано при утечке персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Ничего, это не обязательно',
|
|
|
+ 'Уведомление владельцев данных и контролирующих органов',
|
|
|
+ 'Только смена паролей',
|
|
|
+ 'Увольнение сотрудников'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'В случае утечки владельцы данных должны быть своевременно уведомлены.',
|
|
|
+ 'image': 'q29.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 30,
|
|
|
+ 'question': 'Что такое обезличивание персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Удаление фотографий',
|
|
|
+ 'Действия, после которых нельзя определить принадлежность данных конкретному лицу',
|
|
|
+ 'Шифрование данных',
|
|
|
+ 'Смена имен'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Обезличивание делает невозможным идентификацию человека без использования дополнительной информации.',
|
|
|
+ 'image': 'q30.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 31,
|
|
|
+ 'question': 'Кто осуществляет выбор средств защиты информации для системы защиты ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Роскомнадзор',
|
|
|
+ 'Оператор в соответствии с нормативными актами ФСБ и ФСТЭК',
|
|
|
+ 'Субъект данных',
|
|
|
+ 'Поставщик оборудования'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Выбор средств защиты осуществляется оператором в соответствии с актами ФСБ и ФСТЭК.',
|
|
|
+ 'image': 'q31.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 32,
|
|
|
+ 'question': 'Какие организации могут привлекаться для контроля защиты ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Любые организации',
|
|
|
+ 'Юридические лица и ИП с лицензией на техзащиту информации',
|
|
|
+ 'Только государственные органы',
|
|
|
+ 'Только ФСБ'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Могут привлекаться организации и ИП, имеющие лицензию на техзащиту конфиденциальной информации.',
|
|
|
+ 'image': 'q32.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 33,
|
|
|
+ 'question': 'Что относится к организационным мерам защиты ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Установка файерволов',
|
|
|
+ 'Назначение ответственных лиц, разработка документов, инструктаж сотрудников',
|
|
|
+ 'Шифрование дисков',
|
|
|
+ 'Антивирусная защита'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Организационные меры включают назначение ответственных, разработку документов и обучение сотрудников.',
|
|
|
+ 'image': 'q33.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 34,
|
|
|
+ 'question': 'Что относится к техническим мерам защиты ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Проведение инструктажей',
|
|
|
+ 'Антивирусы, межсетевые экраны, системы шифрования',
|
|
|
+ 'Разработка политик безопасности',
|
|
|
+ 'Подписание согласий'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Технические меры включают программно-аппаратные средства защиты информации.',
|
|
|
+ 'image': 'q34.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 35,
|
|
|
+ 'question': 'Какие данные являются общедоступными?',
|
|
|
+ 'options': [
|
|
|
+ 'Все данные в интернете',
|
|
|
+ 'Данные, доступ к которым предоставлен самим субъектом',
|
|
|
+ 'Паспортные данные',
|
|
|
+ 'Медицинские записи'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Общедоступные данные - это данные, доступ к которым предоставлен самим субъектом.',
|
|
|
+ 'image': 'q35.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 36,
|
|
|
+ 'question': 'Что такое инцидент с персональными данными?',
|
|
|
+ 'options': [
|
|
|
+ 'Плановое обновление ПО',
|
|
|
+ 'Нарушение безопасности, приводящее к утечке или компрометации данных',
|
|
|
+ 'Обычная проверка системы',
|
|
|
+ 'Резервное копирование'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Инцидент - это любое событие, которое привело к нарушению защиты данных.',
|
|
|
+ 'image': 'q36.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 37,
|
|
|
+ 'question': 'Какие требования предъявляются к точности и актуальности данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Данные можно не обновлять',
|
|
|
+ 'Важно обеспечивать правильность и своевременность данных',
|
|
|
+ 'Точность не важна',
|
|
|
+ 'Достаточно собрать данные один раз'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Важно обеспечивать правильность и своевременность данных.',
|
|
|
+ 'image': 'q37.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 38,
|
|
|
+ 'question': 'Что означает требование "законность, справедливость и прозрачность"?',
|
|
|
+ 'options': [
|
|
|
+ 'Данные должны быть доступны всем',
|
|
|
+ 'Обработка должна вестись легально, честно и ясно для граждан',
|
|
|
+ 'Данные должны публиковаться',
|
|
|
+ 'Обработка может быть скрытой'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Обработка должна вестись легально, честно и ясно для граждан, чьи данные собираются.',
|
|
|
+ 'image': 'q38.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 39,
|
|
|
+ 'question': 'Что такое электронный журнал безопасности?',
|
|
|
+ 'options': [
|
|
|
+ 'Список сотрудников',
|
|
|
+ 'Средство регистрации событий доступа и изменений в системе',
|
|
|
+ 'Журнал учета посетителей',
|
|
|
+ 'База данных клиентов'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Электронный журнал регистрирует события доступа и изменения в системе.',
|
|
|
+ 'image': 'q39.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 40,
|
|
|
+ 'question': 'К какому типу данных относится данные о заработной плате?',
|
|
|
+ 'options': [
|
|
|
+ 'Не являются персональными данными',
|
|
|
+ 'Являются персональными данными и не подлежат разглашению без согласия',
|
|
|
+ 'Могут публиковаться свободно',
|
|
|
+ 'Относятся к общедоступным'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Сведения о заработной плате - это персональные данные и не подлежат разглашению без согласия.',
|
|
|
+ 'image': 'q40.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 41,
|
|
|
+ 'question': 'Что должен сделать оператор перед началом обработки ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Ничего',
|
|
|
+ 'Подать уведомление в Роскомнадзор',
|
|
|
+ 'Купить оборудование',
|
|
|
+ 'Нанять сотрудников'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Необходимо подать уведомление о начале обработки ПД в Роскомнадзор для регистрации в качестве оператора.',
|
|
|
+ 'image': 'q41.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 42,
|
|
|
+ 'question': 'Что такое модель угроз для ИСПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Описание возможных злоумышленников',
|
|
|
+ 'Документ, описывающий актуальные угрозы безопасности для конкретной системы',
|
|
|
+ 'Программа для взлома',
|
|
|
+ 'Схема сети'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Модель угроз - документ, описывающий актуальные угрозы для конкретной информационной системы.',
|
|
|
+ 'image': 'q42.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 43,
|
|
|
+ 'question': 'Какие существуют категории персональных данных по 152-ФЗ?',
|
|
|
+ 'options': [
|
|
|
+ 'Только общие и специальные',
|
|
|
+ 'Общие, особые, биометрические и иные данные',
|
|
|
+ 'Только биометрические',
|
|
|
+ 'Только контактные'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Закон выделяет общие, особые, биометрические и иные категории персональных данных.',
|
|
|
+ 'image': 'q43.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 44,
|
|
|
+ 'question': 'Что должно содержать согласие на обработку ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Только подпись',
|
|
|
+ 'Конкретные цели обработки, перечень данных, срок действия',
|
|
|
+ 'Только ФИО',
|
|
|
+ 'Только дату'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Согласие должно быть конкретным, содержать цели обработки, перечень данных и срок действия.',
|
|
|
+ 'image': 'q44.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 45,
|
|
|
+ 'question': 'Кто такой уполномоченный по защите данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Сотрудник полиции',
|
|
|
+ 'Лицо, ответственное за контроль соблюдения законодательства о данных',
|
|
|
+ 'Программист',
|
|
|
+ 'Начальник отдела кадров'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Это лицо, ответственное за контроль соблюдения законодательства о персональных данных.',
|
|
|
+ 'image': 'q45.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 46,
|
|
|
+ 'question': 'Что такое техническое задание на создание системы защиты?',
|
|
|
+ 'options': [
|
|
|
+ 'Рекламный буклет',
|
|
|
+ 'Документ с требованиями к создаваемой системе защиты ПД',
|
|
|
+ 'Инструкция пользователя',
|
|
|
+ 'Договор с поставщиком'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'ТЗ - документ с требованиями по формированию требуемой системы защиты.',
|
|
|
+ 'image': 'q46.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 47,
|
|
|
+ 'question': 'Какие данные относятся к "иным" согласно 152-ФЗ?',
|
|
|
+ 'options': [
|
|
|
+ 'Данные о здоровье',
|
|
|
+ 'Все, что не попадает в общие, особые и биометрические категории',
|
|
|
+ 'Только фотографии',
|
|
|
+ 'Только отпечатки пальцев'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Иные данные - всё, что не попадает в предыдущие категории.',
|
|
|
+ 'image': 'q47.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 48,
|
|
|
+ 'question': 'Что такое ФСТЭК России?',
|
|
|
+ 'options': [
|
|
|
+ 'Производитель компьютеров',
|
|
|
+ 'Федеральная служба по техническому и экспортному контролю',
|
|
|
+ 'Провайдер интернета',
|
|
|
+ 'Страховая компания'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'ФСТЭК - федеральная служба по техническому и экспортному контролю, регулирующая вопросы защиты информации.',
|
|
|
+ 'image': 'q48.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 49,
|
|
|
+ 'question': 'Какие требования предъявляются к помещениям, где обрабатываются ПД?',
|
|
|
+ 'options': [
|
|
|
+ 'Наличие кондиционера',
|
|
|
+ 'Режим, препятствующий неконтролируемому проникновению посторонних',
|
|
|
+ 'Наличие коврового покрытия',
|
|
|
+ 'Яркое освещение'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Необходима организация режима, препятствующего неконтролируемому проникновению посторонних.',
|
|
|
+ 'image': 'q49.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'id': 50,
|
|
|
+ 'question': 'Какова основная цель выполнения требований по защите персональных данных?',
|
|
|
+ 'options': [
|
|
|
+ 'Выполнение формальностей',
|
|
|
+ 'Минимизация вреда от возможной реализации угроз безопасности',
|
|
|
+ 'Экономия средств',
|
|
|
+ 'Повышение престижа компании'
|
|
|
+ ],
|
|
|
+ 'correct': 1,
|
|
|
+ 'explanation': 'Основная цель - минимизация вреда, возникающего из-за возможной реализации угроз безопасности.',
|
|
|
+ 'image': 'q50.jpg',
|
|
|
+ 'bg_color': None
|
|
|
+ }
|
|
|
+] # Остальные вопросы аналогичны, для краткости сократил до 10
|
|
|
+
|
|
|
+class ImageManager:
|
|
|
+ """Класс для управления изображениями"""
|
|
|
+
|
|
|
+ def __init__(self, images_folder: str):
|
|
|
+ self.images_folder = Path(images_folder)
|
|
|
+ self.cache = {} # Кэш для изображений
|
|
|
+ self.pil_available = PIL_AVAILABLE
|
|
|
+
|
|
|
+ def find_image(self, image_name: str) -> Optional[Path]:
|
|
|
+ """Поиск изображения по имени с разными расширениями"""
|
|
|
+ if not image_name:
|
|
|
+ return None
|
|
|
+
|
|
|
+ # Проверка кэша
|
|
|
+ if image_name in self.cache:
|
|
|
+ return self.cache[image_name]
|
|
|
+
|
|
|
+ # Поиск файла
|
|
|
+ image_path = self.images_folder / image_name
|
|
|
+ if image_path.exists():
|
|
|
+ self.cache[image_name] = image_path
|
|
|
+ return image_path
|
|
|
+
|
|
|
+ # Поиск с разными расширениями
|
|
|
+ name_without_ext = image_path.stem
|
|
|
+ for ext in SUPPORTED_FORMATS:
|
|
|
+ test_path = self.images_folder / f"{name_without_ext}{ext}"
|
|
|
+ if test_path.exists():
|
|
|
+ self.cache[image_name] = test_path
|
|
|
+ return test_path
|
|
|
+
|
|
|
+ return None
|
|
|
+
|
|
|
+ def load_image(self, image_path: Path, bg_color: str = '#ffffff', size: Tuple[int, int] = (350, 350)):
|
|
|
+ """Загрузка и обработка изображения"""
|
|
|
+ if not self.pil_available:
|
|
|
+ if image_path.suffix.lower() == '.jpg':
|
|
|
+ return tk.PhotoImage(file=str(image_path))
|
|
|
+ raise Exception("Для работы с изображениями требуется Pillow")
|
|
|
+
|
|
|
+ img = Image.open(image_path)
|
|
|
+
|
|
|
+ # Обработка анимированных GIF
|
|
|
+ if getattr(img, 'is_animated', False):
|
|
|
+ img.seek(0)
|
|
|
+
|
|
|
+ # Обработка прозрачности
|
|
|
+ if img.mode == 'RGBA':
|
|
|
+ bg_rgb = tuple(int(bg_color[i:i+2], 16) for i in (1, 3, 5))
|
|
|
+ background = Image.new('RGB', img.size, bg_rgb)
|
|
|
+ background.paste(img, mask=img.split()[3])
|
|
|
+ img = background
|
|
|
+ elif img.mode != 'RGB':
|
|
|
+ img = img.convert('RGB')
|
|
|
+
|
|
|
+ # Изменение размера
|
|
|
+ img.thumbnail(size, Image.Resampling.LANCZOS)
|
|
|
+ return ImageTk.PhotoImage(img)
|
|
|
+
|
|
|
+ def get_dominant_color(self, image_path: Path) -> Optional[str]:
|
|
|
+ """Получение доминирующего цвета изображения"""
|
|
|
+ if not self.pil_available:
|
|
|
+ return None
|
|
|
+
|
|
|
+ try:
|
|
|
+ img = Image.open(image_path)
|
|
|
+
|
|
|
+ if getattr(img, 'is_animated', False):
|
|
|
+ img.seek(0)
|
|
|
+
|
|
|
+ # Конвертация в RGB
|
|
|
+ if img.mode == 'RGBA':
|
|
|
+ background = Image.new('RGB', img.size, (255, 255, 255))
|
|
|
+ background.paste(img, mask=img.split()[3])
|
|
|
+ img = background
|
|
|
+ elif img.mode != 'RGB':
|
|
|
+ img = img.convert('RGB')
|
|
|
+
|
|
|
+ # Уменьшение для ускорения
|
|
|
+ img.thumbnail((100, 100))
|
|
|
+ pixels = img.getdata()
|
|
|
+
|
|
|
+ # Вычисление среднего цвета
|
|
|
+ r_sum = g_sum = b_sum = 0
|
|
|
+ for pixel in pixels:
|
|
|
+ r_sum += pixel[0]
|
|
|
+ g_sum += pixel[1]
|
|
|
+ b_sum += pixel[2]
|
|
|
+
|
|
|
+ count = len(pixels)
|
|
|
+ if count == 0:
|
|
|
+ return None
|
|
|
+
|
|
|
+ return f'#{r_sum//count:02x}{g_sum//count:02x}{b_sum//count:02x}'
|
|
|
+ except Exception as e:
|
|
|
+ print(f"Ошибка определения цвета: {e}")
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+class QuizApp:
|
|
|
+ """Основной класс приложения"""
|
|
|
+
|
|
|
+ def __init__(self, master):
|
|
|
+ self.master = master
|
|
|
+ self.master.title("Викторина: Защита персональных данных")
|
|
|
+ self.master.geometry(WINDOW_SIZE)
|
|
|
+ self.master.resizable(False, False)
|
|
|
+
|
|
|
+ # Инициализация
|
|
|
+ self.questions = questions.copy()
|
|
|
+ self.current_question = 0
|
|
|
+ self.correct_answers = 0
|
|
|
+ self.total_questions = len(self.questions)
|
|
|
+
|
|
|
+ # Менеджеры
|
|
|
+ self.image_manager = ImageManager(IMAGES_FOLDER)
|
|
|
+
|
|
|
+ # Переменные состояния
|
|
|
+ self.var = None
|
|
|
+ self.option_mapping = {}
|
|
|
+ self.current_image = None
|
|
|
+
|
|
|
+ # Привязка клавиш
|
|
|
+ self._bind_keys()
|
|
|
+
|
|
|
+ # Запуск
|
|
|
+ self.show_start_screen()
|
|
|
+
|
|
|
+ def _bind_keys(self):
|
|
|
+ """Привязка клавиатурных комбинаций"""
|
|
|
+ self.master.bind('<Key>', self.on_key_press)
|
|
|
+ self.master.bind('<Return>', lambda e: self.check_answer())
|
|
|
+ self.master.bind('<Escape>', lambda e: self.quit_app())
|
|
|
+
|
|
|
+ def on_key_press(self, event):
|
|
|
+ """Обработка нажатий клавиш"""
|
|
|
+ if event.char in '1234' and self.var is not None:
|
|
|
+ self.var.set(int(event.char) - 1)
|
|
|
+
|
|
|
+ def quit_app(self):
|
|
|
+ """Выход из приложения"""
|
|
|
+ if messagebox.askyesno("Выход", "Вы действительно хотите выйти?"):
|
|
|
+ self.master.quit()
|
|
|
+
|
|
|
+ def clear_window(self):
|
|
|
+ """Очистка окна"""
|
|
|
+ for widget in self.master.winfo_children():
|
|
|
+ widget.destroy()
|
|
|
+
|
|
|
+ def shuffle_questions(self):
|
|
|
+ """Перемешивание вопросов"""
|
|
|
+ random.shuffle(self.questions)
|
|
|
+
|
|
|
+ def get_text_color(self, bg_hex: str) -> str:
|
|
|
+ """Определение цвета текста на основе фона"""
|
|
|
+ if not bg_hex:
|
|
|
+ return 'black'
|
|
|
+ try:
|
|
|
+ r = int(bg_hex[1:3], 16)
|
|
|
+ g = int(bg_hex[3:5], 16)
|
|
|
+ b = int(bg_hex[5:7], 16)
|
|
|
+ brightness = 0.299 * r + 0.587 * g + 0.114 * b
|
|
|
+ return 'black' if brightness > 128 else 'white'
|
|
|
+ except:
|
|
|
+ return 'black'
|
|
|
+
|
|
|
+ def show_start_screen(self):
|
|
|
+ """Экран приветствия"""
|
|
|
+ self.clear_window()
|
|
|
+ self.master.config(bg='lightblue')
|
|
|
+
|
|
|
+ # Заголовок
|
|
|
+ title = tk.Label(self.master, text="Викторина по защите персональных данных",
|
|
|
+ font=("Arial", 20, "bold"), bg='lightblue', fg='navy')
|
|
|
+ title.pack(pady=40)
|
|
|
+
|
|
|
+ # Инструкция
|
|
|
+ instr_text = (
|
|
|
+ f"Добро пожаловать!\n\n"
|
|
|
+ f"Эта викторина проверит ваши знания об основах защиты персональных данных.\n"
|
|
|
+ f"Правила:\n"
|
|
|
+ f"• Всего вопросов: {self.total_questions}\n"
|
|
|
+ f"• Выберите один правильный вариант.\n"
|
|
|
+ f"• Управление с клавиатуры: клавиши 1-4 для выбора ответа\n"
|
|
|
+ f"• Нажмите Enter для проверки ответа\n"
|
|
|
+ f"• Esc для выхода\n"
|
|
|
+ f"• После ответа вы увидите пояснение.\n\n"
|
|
|
+ f"Удачи!"
|
|
|
+ )
|
|
|
+
|
|
|
+ instr_label = tk.Label(self.master, text=instr_text, font=("Arial", 12),
|
|
|
+ bg='lightblue', justify='left')
|
|
|
+ instr_label.pack(pady=20)
|
|
|
+
|
|
|
+ # Кнопка старта
|
|
|
+ start_btn = tk.Button(self.master, text="Начать викторину", font=("Arial", 14),
|
|
|
+ command=self.start_quiz, bg='green', fg='white',
|
|
|
+ padx=20, pady=10, cursor='hand2')
|
|
|
+ start_btn.pack(pady=30)
|
|
|
+
|
|
|
+ def start_quiz(self):
|
|
|
+ """Начало викторины"""
|
|
|
+ self.shuffle_questions()
|
|
|
+ self.current_question = 0
|
|
|
+ self.correct_answers = 0
|
|
|
+ self.show_question()
|
|
|
+
|
|
|
+ def get_background_color(self, q: Dict, index: int) -> str:
|
|
|
+ """Получение цвета фона для вопроса"""
|
|
|
+ if q.get('bg_color'):
|
|
|
+ return q['bg_color']
|
|
|
+
|
|
|
+ if PIL_AVAILABLE and q.get('image'):
|
|
|
+ image_path = self.image_manager.find_image(q['image'])
|
|
|
+ if image_path:
|
|
|
+ color = self.image_manager.get_dominant_color(image_path)
|
|
|
+ if color:
|
|
|
+ return color
|
|
|
+
|
|
|
+ return DEFAULT_COLORS[index % len(DEFAULT_COLORS)]
|
|
|
+
|
|
|
+ def create_option_buttons(self, parent, options: List, bg_color: str, text_color: str):
|
|
|
+ """Создание кнопок вариантов ответов"""
|
|
|
+ options_with_indices = list(enumerate(options))
|
|
|
+ random.shuffle(options_with_indices)
|
|
|
+
|
|
|
+ self.var = tk.IntVar(value=-1)
|
|
|
+ self.option_mapping = {}
|
|
|
+
|
|
|
+ # Функции для эффекта наведения
|
|
|
+ def on_enter(btn):
|
|
|
+ btn.config(bg='#d0d0d0' if text_color == 'black' else '#505050')
|
|
|
+
|
|
|
+ def on_leave(btn, orig_bg):
|
|
|
+ btn.config(bg=orig_bg)
|
|
|
+
|
|
|
+ for new_idx, (old_idx, option_text) in enumerate(options_with_indices):
|
|
|
+ self.option_mapping[new_idx] = old_idx
|
|
|
+ display_text = f"{new_idx + 1}. {option_text}"
|
|
|
+
|
|
|
+ rb = tk.Radiobutton(parent, text=display_text, variable=self.var,
|
|
|
+ value=new_idx, font=("Arial", 11), wraplength=350,
|
|
|
+ justify='left', indicatoron=0, bg=bg_color,
|
|
|
+ fg=text_color, selectcolor=bg_color,
|
|
|
+ padx=15, pady=8, cursor='hand2')
|
|
|
+ rb.pack(anchor='center', pady=5)
|
|
|
+
|
|
|
+ # Эффект наведения
|
|
|
+ orig_bg = rb.cget('bg')
|
|
|
+ rb.bind("<Enter>", lambda e, b=rb: on_enter(b))
|
|
|
+ rb.bind("<Leave>", lambda e, b=rb: on_leave(b, orig_bg))
|
|
|
+
|
|
|
+ def show_question(self):
|
|
|
+ """Отображение вопроса"""
|
|
|
+ self.clear_window()
|
|
|
+
|
|
|
+ # Удаление старого изображения
|
|
|
+ if hasattr(self, 'current_image'):
|
|
|
+ del self.current_image
|
|
|
+
|
|
|
+ q = self.questions[self.current_question]
|
|
|
+ bg_color = self.get_background_color(q, self.current_question)
|
|
|
+ text_color = self.get_text_color(bg_color)
|
|
|
+ self.master.config(bg=bg_color)
|
|
|
+
|
|
|
+ # Основной контейнер
|
|
|
+ main_frame = tk.Frame(self.master, bg=bg_color)
|
|
|
+ main_frame.pack(fill='both', expand=True, padx=30, pady=20)
|
|
|
+
|
|
|
+ # Левая колонка
|
|
|
+ left_frame = tk.Frame(main_frame, bg=bg_color)
|
|
|
+ left_frame.pack(side='left', fill='both', expand=True)
|
|
|
+
|
|
|
+ # Правая колонка
|
|
|
+ right_frame = tk.Frame(main_frame, bg=bg_color)
|
|
|
+ right_frame.pack(side='right', fill='both', expand=True)
|
|
|
+
|
|
|
+ # Вопрос
|
|
|
+ question_lbl = tk.Label(left_frame, text=q['question'],
|
|
|
+ font=("Arial", 14, "bold"), wraplength=450,
|
|
|
+ justify='left', bg=bg_color, fg=text_color)
|
|
|
+ question_lbl.pack(anchor='nw', pady=(0, 20))
|
|
|
+
|
|
|
+ # Изображение
|
|
|
+ if q.get('image'):
|
|
|
+ image_path = self.image_manager.find_image(q['image'])
|
|
|
+ if image_path:
|
|
|
+ try:
|
|
|
+ self.current_image = self.image_manager.load_image(image_path, bg_color)
|
|
|
+ img_label = tk.Label(left_frame, image=self.current_image, bg=bg_color)
|
|
|
+ img_label.pack(anchor='center', pady=10)
|
|
|
+ except Exception as e:
|
|
|
+ error_text = f"[Ошибка: {image_path.name}]\n{str(e)[:50]}"
|
|
|
+ error_label = tk.Label(left_frame, text=error_text,
|
|
|
+ font=("Arial", 8), fg='red', bg=bg_color)
|
|
|
+ error_label.pack(anchor='center')
|
|
|
+ else:
|
|
|
+ error_text = f"[Файл не найден: {q['image']}]\nПоддерживаемые форматы: PNG, JPG, JPEG, GIF, BMP"
|
|
|
+ error_label = tk.Label(left_frame, text=error_text,
|
|
|
+ font=("Arial", 8), fg='orange', bg=bg_color)
|
|
|
+ error_label.pack(anchor='center')
|
|
|
+
|
|
|
+ # Прогресс и подсказки
|
|
|
+ progress_lbl = tk.Label(left_frame,
|
|
|
+ text=f"Вопрос {self.current_question + 1} из {self.total_questions}",
|
|
|
+ font=("Arial", 11), fg=text_color, bg=bg_color)
|
|
|
+ progress_lbl.pack(side='bottom', anchor='sw', pady=10)
|
|
|
+
|
|
|
+ hint_lbl = tk.Label(left_frame,
|
|
|
+ text="Клавиши 1-4 → выбор ответа, Enter → проверка",
|
|
|
+ font=("Arial", 9), fg=text_color, bg=bg_color)
|
|
|
+ hint_lbl.pack(side='bottom', anchor='sw', pady=5)
|
|
|
+
|
|
|
+ # Варианты ответов
|
|
|
+ center_container = tk.Frame(right_frame, bg=bg_color)
|
|
|
+ center_container.pack(expand=True)
|
|
|
+
|
|
|
+ options_title = tk.Label(center_container, text="Выберите ответ:",
|
|
|
+ font=("Arial", 12, "bold"), bg=bg_color, fg=text_color)
|
|
|
+ options_title.pack(pady=(0, 15))
|
|
|
+
|
|
|
+ self.create_option_buttons(center_container, q['options'], bg_color, text_color)
|
|
|
+
|
|
|
+ # Кнопки управления
|
|
|
+ btn_frame = tk.Frame(right_frame, bg=bg_color)
|
|
|
+ btn_frame.pack(side='bottom', fill='x', pady=20)
|
|
|
+
|
|
|
+ exit_btn = tk.Button(btn_frame, text="Выход (Esc)", font=("Arial", 11),
|
|
|
+ command=self.quit_app, bg='red', fg='white',
|
|
|
+ padx=20, pady=5, cursor='hand2')
|
|
|
+ exit_btn.pack(side='right', padx=10)
|
|
|
+
|
|
|
+ check_btn = tk.Button(btn_frame, text="Проверить (Enter)", font=("Arial", 11),
|
|
|
+ command=self.check_answer, bg='blue', fg='white',
|
|
|
+ padx=20, pady=5, cursor='hand2')
|
|
|
+ check_btn.pack(side='right', padx=10)
|
|
|
+
|
|
|
+ self.master.focus_set()
|
|
|
+
|
|
|
+ def check_answer(self):
|
|
|
+ """Проверка ответа"""
|
|
|
+ if self.var is None or self.var.get() == -1:
|
|
|
+ messagebox.showwarning("Нет выбора", "Пожалуйста, выберите один из вариантов (клавиши 1-4).")
|
|
|
+ return
|
|
|
+
|
|
|
+ selected = self.var.get()
|
|
|
+ original_selected = self.option_mapping[selected]
|
|
|
+ q = self.questions[self.current_question]
|
|
|
+
|
|
|
+ if original_selected == q['correct']:
|
|
|
+ self.correct_answers += 1
|
|
|
+ self.show_feedback("Правильно!", "green")
|
|
|
+ else:
|
|
|
+ correct_text = q['options'][q['correct']]
|
|
|
+ explanation = f"Неправильно. Правильный ответ: {correct_text}\n\n{q['explanation']}"
|
|
|
+ self.show_feedback("Неправильно", "red", explanation)
|
|
|
+
|
|
|
+ def show_feedback(self, message: str, color: str, explanation: str = None):
|
|
|
+ """Отображение обратной связи"""
|
|
|
+ win = tk.Toplevel(self.master)
|
|
|
+ win.title("Результат")
|
|
|
+ win.geometry("500x350")
|
|
|
+ win.configure(bg=color)
|
|
|
+ win.transient(self.master)
|
|
|
+ win.grab_set()
|
|
|
+
|
|
|
+ # Закрытие окна
|
|
|
+ def close_and_continue():
|
|
|
+ win.destroy()
|
|
|
+ self.next_question()
|
|
|
+
|
|
|
+ win.protocol("WM_DELETE_WINDOW", close_and_continue)
|
|
|
+ win.bind('<Return>', lambda e: close_and_continue())
|
|
|
+ win.bind('<Escape>', lambda e: close_and_continue())
|
|
|
+
|
|
|
+ # Сообщение
|
|
|
+ lbl = tk.Label(win, text=message, bg=color, fg='white',
|
|
|
+ font=("Arial", 24, "bold"), wraplength=450, justify='center')
|
|
|
+ lbl.pack(expand=True, fill='both', padx=20, pady=20)
|
|
|
+
|
|
|
+ # Пояснение
|
|
|
+ if explanation:
|
|
|
+ expl_lbl = tk.Label(win, text=explanation, bg=color, fg='white',
|
|
|
+ font=("Arial", 11), wraplength=450, justify='left')
|
|
|
+ expl_lbl.pack(padx=20, pady=10)
|
|
|
+
|
|
|
+ # Кнопка продолжения
|
|
|
+ btn = tk.Button(win, text="Продолжить (Enter)", command=close_and_continue,
|
|
|
+ bg='white', font=("Arial", 11), padx=20, pady=5, cursor='hand2')
|
|
|
+ btn.pack(pady=20)
|
|
|
+
|
|
|
+ win.focus_set()
|
|
|
+
|
|
|
+ def next_question(self):
|
|
|
+ """Переход к следующему вопросу"""
|
|
|
+ if self.current_question + 1 < self.total_questions:
|
|
|
+ self.current_question += 1
|
|
|
+ self.show_question()
|
|
|
+ else:
|
|
|
+ self.show_result()
|
|
|
+
|
|
|
+ def show_result(self):
|
|
|
+ """Отображение результатов"""
|
|
|
+ self.clear_window()
|
|
|
+ self.master.config(bg='lightgreen')
|
|
|
+
|
|
|
+ percentage = (self.correct_answers / self.total_questions) * 100
|
|
|
+ result_text = f"Викторина завершена!\n\n"
|
|
|
+ result_text += f"Вы ответили правильно на {self.correct_answers} из {self.total_questions} вопросов.\n"
|
|
|
+ result_text += f"Ваш результат: {percentage:.1f}%\n\n"
|
|
|
+
|
|
|
+ if percentage == 100:
|
|
|
+ result_text += "Отлично! Вы очень хорошо усвоили материал."
|
|
|
+ elif percentage >= 80:
|
|
|
+ result_text += "Отличный результат! Вы хорошо знаете тему."
|
|
|
+ elif percentage >= 60:
|
|
|
+ result_text += "Хороший результат! Есть куда стремиться."
|
|
|
+ else:
|
|
|
+ result_text += "Стоит повторить лекцию и попробовать снова."
|
|
|
+
|
|
|
+ lbl = tk.Label(self.master, text=result_text, font=("Arial", 16),
|
|
|
+ bg='lightgreen', justify='center')
|
|
|
+ lbl.pack(pady=30)
|
|
|
+
|
|
|
+ # Привязка клавиш
|
|
|
+ self.master.bind('<Return>', lambda e: self.start_quiz())
|
|
|
+ self.master.bind('<Escape>', lambda e: self.quit_app())
|
|
|
+
|
|
|
+ # Подсказка
|
|
|
+ hint_lbl = tk.Label(self.master, text="Enter - пройти заново | Esc - выход",
|
|
|
+ font=("Arial", 11), bg='lightgreen', fg='gray')
|
|
|
+ hint_lbl.pack(pady=10)
|
|
|
+
|
|
|
+ # Кнопки
|
|
|
+ btn_frame = tk.Frame(self.master, bg='lightgreen')
|
|
|
+ btn_frame.pack(pady=20)
|
|
|
+
|
|
|
+ restart_btn = tk.Button(btn_frame, text="Пройти заново (Enter)",
|
|
|
+ font=("Arial", 12), command=self.start_quiz,
|
|
|
+ bg='blue', fg='white', padx=20, pady=8, cursor='hand2')
|
|
|
+ restart_btn.pack(side='left', padx=15)
|
|
|
+
|
|
|
+ exit_btn = tk.Button(btn_frame, text="Завершить (Esc)",
|
|
|
+ font=("Arial", 12), command=self.quit_app,
|
|
|
+ bg='orange', fg='white', padx=20, pady=8, cursor='hand2')
|
|
|
+ exit_btn.pack(side='left', padx=15)
|
|
|
+
|
|
|
+ self.master.focus_set()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ root = tk.Tk()
|
|
|
+ app = QuizApp(root)
|
|
|
+ root.mainloop()
|