ИНФОРМАТИКА · 11 КЛАСС
Оконные приложения · Интерактивный конспект

§ 3. Проектирование интерфейса оконного приложения с использованием элементов управления

Выберем подходящие элементы управления, построим форму ввода и расчёта, проверим числа и различия между флажками и переключателями.

Попробовать на модели ↓
Разбираемся в теме

Основные пункты параграфа

1

Назначение компонентов

Кнопка запускает действие, надпись поясняет результат, текстовое поле принимает данные.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Назначение компонентов

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

Button используют для команды, Label — для подписи, TextBox — для ввода и показа текста. ListBox и ComboBox подходят для выбора из списка, PictureBox — для изображения. Внятная подпись должна объяснять, что вводить и в каких единицах. Name делают понятным для кода, Text — для пользователя.

Enabled=False оставляет элемент видимым, но отключает взаимодействие. Visible=False скрывает его. ReadOnly у текстового поля запрещает пользовательское редактирование, сохраняя возможность программно обновить результат. Это разные способы управления интерфейсом, а не взаимозаменяемые свойства.

Главная мысль: Кнопка запускает действие, надпись поясняет результат, текстовое поле принимает данные.
2

Текст, ввод и события

Ввод обрабатывают после нужного события, учитывая пустую строку и доступность команды.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Текст, ввод и события

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

Text содержит строку даже тогда, когда пользователь набрал цифры. TextChanged сообщает об изменении текста; Click кнопки может запускать расчёт. Для ввода по Enter можно обработать подходящее клавиатурное событие. События клавиатуры получает элемент с фокусом, поэтому порядок перехода клавишей Tab тоже важен.

Приветствие строят из проверенного имени и постоянного текста. Пустое поле не следует превращать в бессмысленный ответ. Лучше показать краткое пояснение у ввода. В тренажёре можно набрать учебное имя; оно не отправляется в Интернет и существует только в текущем опыте.

Главная мысль: Ввод обрабатывают после нужного события, учитывая пустую строку и доступность команды.
3

Преобразование и проверка чисел

Перед вычислением текст преобразуют в число и проверяют область допустимых значений.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Преобразование и проверка чисел

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

Преобразование может не удаться из-за пустой строки, букв или неподходящего разделителя. Правила дробного разделителя зависят от способа преобразования и локали. В модели допускается одна запятая или точка, но запрещаются лишние знаки и частично прочитанные значения вроде 12abc.

Квадрат вычисляют как x*x. Квадратный корень в вещественных числах требует x≥0; деление — ненулевого делителя. Целочисленное деление применяют к целым, обычное может давать дробь. Например, 7 div 2=3, а 7/2=3,5. Удобно явно показать неверное поле и сохранить введённые данные для исправления.

{ Псевдокод безопасного расчёта }
если текст не преобразуется в число:
    показать ошибку ввода
иначе если число < 0:
    сообщить, что вещественный корень не определён
иначе:
    вывести число * число и sqrt(число)
Главная мысль: Перед вычислением текст преобразуют в число и проверяют область допустимых значений.
4

Флажки, переключатели и группы

Флажки выбирают независимые возможности, переключатели — одну альтернативу внутри группы.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Флажки, переключатели и группы

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

CheckBox хранит состояние Checked и подходит для независимого «да/нет», например включения подробного объяснения. Некоторые флажки поддерживают третье состояние, но в учебном опыте используются два. RadioButton выбирают одну из взаимоисключающих альтернатив: обычное либо целочисленное деление.

Переключатели объединяют общим контейнером, например GroupBox, чтобы один выбор не отменял другой в независимом вопросе. Состояние элементов считывают при расчёте. Проверьте каждую комбинацию, смену режима после ввода и попытку вычисления при нулевом делителе.

Главная мысль: Флажки выбирают независимые возможности, переключатели — одну альтернативу внутри группы.
Интерактивная практика

Исследуйте и примените

Проверьте модель на нескольких наборах данных, затем выполните самостоятельное задание.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Лаборатория: проверьте свой вариант

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

Соберём главное

Шесть выводов

01

Кнопка запускает действие, надпись поясняет результат, текстовое поле принимает данные.

02

Ввод обрабатывают после нужного события, учитывая пустую строку и доступность команды.

03

Перед вычислением текст преобразуют в число и проверяют область допустимых значений.

04

Флажки выбирают независимые возможности, переключатели — одну альтернативу внутри группы.

05

Пустой или неверный ввод обрабатывают до вычисления.

06

Проверяйте управление клавиатурой и читаемость подписей, а не только щелчки мышью.

Самопроверка

Тест: 10 вопросов

Один верный ответ. За ответ — 0,5 балла. Откройте и проверьте себя.

1. Какой компонент запускает команду?

2. Что хранится в TextBox.Text?

3. ReadOnly запрещает…

4. Enabled=False обычно делает элемент…

5. Какое событие связано с изменением текста?

6. Что проверить перед преобразованием?

7. Чему равно 7/2 при обычном делении?

8. Чему равно 7 div 2?

9. Для независимых настроек подходят…

10. Для одного выбора внутри группы подходят…

ВыводыТестВ началоВсе параграфы
↑

Загрузка прогресса…

\n'));msg='title относится к метаданным документа, h1 — к видимому содержимому. Текст безопасно выводится как текст.';return} input('path','Фрагмент ссылки',30);html=pad(code('Перейти\n

Раздел

')+stats([['Совпадение фрагмента и id','одинаковые записи'],['Проверка учебного идентификатора',/^[a-zA-Z][\w-]*$/.test(S.path)?'подходит':'начните с латинской буквы; затем буквы, цифры, _ или -']]));msg='Это упрощённый фильтр для учебных примеров, не полная грамматика HTML id. Идентификатор должен быть уникален на странице.';} function editor(){if(m===1){next();html=flow(['Написать HTML','Сохранить .html в UTF-8','Открыть в браузере','Исправить и обновить'],S.step%4)+pad('

'+['Код описывает структуру.','Проверьте, что имя не заканчивается на .html.txt.','Браузер отображает документ, а не редакторский проект.','После изменения файла обновите открытую страницу.'][S.step%4]+'

');msg='Редактор и браузер выполняют разные функции.';return} if(m===2){check('aside','Добавить aside');html=pad('
header — заголовок сайта
main — основное содержание
'+(S.aside?'':'')+'
'+code('
…
\n\n
…
\n'+(S.aside?'\n':'')+''));msg='Смысловые элементы задают назначение областей. Их расположение определяется CSS.';return} if(m===4){select('path','Относительная ссылка',[['images/logo.svg','images/logo.svg'],['../index.html','../index.html'],['#team','#team'],['contacts.html','contacts.html']]);html=pad(stats([['Текущая страница','https://school.example/club/about.html'],['Результат',new URL(S.path,'https://school.example/club/about.html').href]])+code('Ссылка'));msg='Адрес вычисляется относительно текущей страницы. Модель не обращается к сайту; ../ поднимает на один уровень.';return} area('source','Редактор фрагмента HTML');html=pad('
'+cleanHTML(S.source,'preview-'+m+'-')+'
');msg='Поддерживаются заголовки, абзацы, списки, смысловые области, выделение и внутренние ссылки. Скрипты, стили, внешние ссылки и опасные элементы удаляются из предпросмотра. Это ограниченный учебный редактор.';} function css(){if(m===1||m===3){range('size','Размер текста, px',14,36);select('color','Цвет текста',[['#294d3a','Тёмно-зелёный'],['#704522','Коричневый'],['#304f76','Синий']]);html=pad('

Стиль меняет представление

'+code('.note {\n color: '+S.color+';\n font-size: '+S.size+'px;\n}'));msg=m===1?'Селектор выбирает элемент, декларации задают свойства. Число и px записываются слитно.':'Одно правило можно вынести во внешний файл и применить на нескольких страницах. Модель показывает результат деклараций.';return} if(m===2||m===5){check('classRule','Правило класса .note');check('idRule','Правило идентификатора #sample');check('inlineRule','Обычный встроенный style');check('late','Позднее правило класса');let winner=S.inlineRule?'style':S.idRule?'#sample':S.classRule?S.late?'.note (позднее)':'.note':'p',color=S.inlineRule?'#304f76':S.idRule?'#704522':S.classRule?S.late?'#654777':'#34683e':'#294d3a',uid='css-sample-'+m;let rules=`#vis-${m} .css-example p {color:#294d3a}`+(S.classRule?`\n#vis-${m} .css-example .note {color:#34683e}`:'')+(S.idRule?`\n#vis-${m} .css-example #${uid} {color:#704522}`:'')+(S.classRule&&S.late?`\n#vis-${m} .css-example .note {color:#654777}`:'');html=pad('

Какой цвет победит?

'+stats([['Победитель',winner],['Ожидаемый цвет',color]])+code('p { color: #294d3a; }\n'+(S.classRule?'.note { color: #34683e; }\n':'')+(S.idRule?'#sample { color: #704522; }\n':'')+(S.classRule&&S.late?'.note { color: #654777; }\n':'')+(S.inlineRule?'style="color:#304f76"':'')));msg='Условия опыта: обычные авторские правила одного слоя, без !important, анимаций и переходов. При равной специфичности побеждает более позднее правило.';return} range('width','Заданная width, px',100,300);range('padding','Padding с каждой стороны, px',0,30);range('border','Border с каждой стороны, px',0,10);select('box','box-sizing',[['content-box','content-box'],['border-box','border-box']]);let outer=S.box==='content-box'?S.width+2*S.padding+2*S.border:S.width,inner=S.box==='content-box'?S.width:S.width-2*S.padding-2*S.border;html=pad('
Содержимое
'+stats([['Содержимое',inner+' px'],['Полная ширина',outer+' px']])+code('width: '+S.width+'px;\npadding: '+S.padding+'px;\nborder: '+S.border+'px solid;\nbox-sizing: '+S.box+';'));msg='Расчёт по горизонтали: содержимое + два отступа + две границы. Внешние margin в эту ширину не входят.';} function media(){if(m===5)select('kind','Медиа',[['image','Изображение'],['audio','Звук'],['video','Видео']]);if(m===1){check('decorative','Изображение только декоративное');input('alt','Текст alt',100);html=pad(''+esc(S.decorative?'':S.alt)+''+code(''+(S.decorative?'':S.alt)+'')+'

Текстовая альтернатива: '+esc(S.decorative?'пустая — декор пропускается':S.alt||'не задана')+'

');msg=S.decorative?'Пустой alt уместен для декора, когда изображение не добавляет смысла.':'Опишите смысл изображения в контексте; слово «картинка» само по себе не помогает.';return} if(S.kind==='image'){select('fit','object-fit',[['contain','contain'],['cover','cover'],['fill','fill']]);html=pad('
Пейзаж: горы и солнце
'+stats([['Источник','400 × 200'],['Рамка','200 × 200'],['Изображение',S.fit==='contain'?'200 × 100':S.fit==='cover'?'400 × 200, обрезка':'200 × 200, искажение']]));msg='contain сохраняет всё изображение, cover заполняет рамку с обрезкой, fill меняет пропорции.';return} let audio=S.kind==='audio';html=pad('<'+(audio?'audio':'video')+' class="local-media" controls preload="metadata" '+(!audio?'playsinline':'')+' src="'+(audio?'':'')+'">

'+(audio?'Звук: негромкий тон 440 Гц, 2 секунды.':'Видео: жёлтый круг движется по зелёному фону, 3 секунды; без звука.')+'

'+code('<'+(audio?'audio':'video')+' controls preload="metadata">\n \n'));msg='Нажмите встроенную кнопку воспроизведения. Запись включена в HTML; Интернет не нужен. Запуск звука остаётся под вашим управлением. Если формат не поддерживается браузером, доступно текстовое описание.';} function builder(){input('title','Название сайта',40);select('theme','Общий стиль',[['green','Зелёный'],['sand','Песочный'],['blue','Голубой']]);range('previewWidth','Ширина предпросмотра, px',240,600,10);select('selected','Блок для перестановки',S.blocks.map((x,i)=>[i,x]));ct+=btn('up','Поднять блок')+btn('down','Опустить блок');if(m>=3)check('aside','Показывать блок контактов');html=pad('
'+esc(S.title)+'
'+S.blocks.filter(x=>S.aside||x!=='Контакты').map(x=>'
'+esc(x)+'

'+({'О клубе':'Встречаемся, учимся и создаём.','Проекты':'Делимся результатами работы.','Контакты':'Как найти нашу команду.'}[x])+'

').join('')+'
'+stats([['Блоков',S.blocks.filter(x=>S.aside||x!=='Контакты').length],['Колонок',S.previewWidth<400?1:2]]));msg=['Визуальный конструктор собирает страницу из блоков. Измените заголовок и сравните результат.','Шаблон задаёт общую структуру; содержание нужно подготовить самостоятельно.','Меняйте порядок блоков. Общая тема применяется ко всему макету.','Проверьте узкую ширину: карточки перестраиваются в одну колонку.','Это локальный макет. Предпросмотр не публикует сайт в Интернете.'][m-1];} function site(){if(m===1){select('kind','Аудитория',[['','Новые участники'],['parents','Родители'],['team','Команда проекта']]);let k={'':0,parents:1,team:2}[S.kind];html=flow(['Аудитория','Задача','Раздел','Проверка'],S.step%4)+pad(table(['Решение','Пример'],[['Задача',['Понять, как вступить','Узнать расписание','Найти материалы'][k]],['Нужный раздел',['О клубе и контакты','Расписание','Проекты и ресурсы'][k]],['Критерий',['Путь к контакту найден','Время и место понятны','Материал открывается'][k]]]));next();msg='Структура сайта зависит от конкретной задачи посетителя.';return} check('links','Главная → Проекты');check('back','Проекты → Контакты');if(m>=4){check('goal','Проверено содержание');check('mobile','Проверена узкая версия');}let reachable=['Главная',...(S.links?['Проекты']:[]),...(S.links&&S.back?['Контакты']:[])];html=svg([80,320,560].map((x,i)=>''+tx(x,137,['Главная','Проекты','Контакты'][i],15)).join('')+''+tx(200,105,S.links?'есть ссылка':'нет ссылки',13)+tx(440,105,S.back?'есть ссылка':'нет ссылки',13))+pad(stats([['Достижимы с главной',reachable.length+' из 3'],['Не достижимы',['Главная','Проекты','Контакты'].filter(x=>!reachable.includes(x)).join(', ')||'нет']])+(m>=4?table(['Проверка','Результат'],[['Все разделы достижимы',reachable.length===3?'да':'нет'],['Содержание',S.goal?'проверено':'нужна проверка'],['Мобильная версия',S.mobile?'проверено':'нужна проверка']]):''));msg='В схеме только показанные направленные ссылки. Доступность файла сама по себе не означает, что посетитель найдёт путь к нему. Контрольный список не гарантирует отсутствие всех ошибок.';} function systems(){if(m===1||m===2||m===5){input('query','Поиск по названию',35);select('category','Раздел',[['all','Все'],['code','Программирование'],['web','Веб'],['society','Общество']]);check('available','Только доступные');const books=[['HTML с примерами','web',true],['Алгоритмы','code',true],['Информация и человек','society',false],['CSS на практике','web',false],['Язык Pascal','code',true]];let found=books.filter(x=>x[0].toLowerCase().includes(S.query.toLowerCase())&&(S.category==='all'||x[1]===S.category)&&(!S.available||x[2]));html=flow(['Данные каталога','Условия запроса','Обработка','Результат'],3)+pad(table(['Название','Раздел','Доступно'],found.map(x=>[x[0],{web:'Веб',code:'Программирование',society:'Общество'}[x[1]],x[2]?'да':'нет']))+stats([['Всего записей',books.length],['Найдено',found.length]])+'

Учебный вымышленный каталог. Данные находятся внутри страницы.

');msg='Фильтры соединены условием И. Система включает не только таблицу, но и правила, программное обеспечение и работу людей.';return} if(m===3){select('kind','Технология',[['cloud','Облачная услуга'],['grid','Распределённые вычисления'],['ar','Дополненная реальность'],['vr','Виртуальная реальность']]);let a={cloud:['Удалённый ресурс по сети','Сервис предоставляет хранение или обработку; условия доступа и защиты проверяют отдельно.'],grid:['Части вычислительной задачи','Несколько узлов совместно обрабатывают распределённую работу.'],ar:['Реальная среда + цифровой слой','Подсказки накладываются на вид окружающего мира.'],vr:['Искусственная среда','Пользователь взаимодействует с виртуальным пространством.']}[S.kind];html=flow(['Задача','Технология','Результат'],1)+pad('
'+a[0]+'

'+a[1]+'

');msg='Выбирают технологию по задаче и ограничениям, а не по названию.';return} choices('Библиотека предоставляет читателю поиск по электронному каталогу. Что является услугой?',['Сам поиск, предоставленный читателю','Набор записей каталога','Готовый справочник'],0,'Каталог — ресурс, готовый справочник может быть продуктом, предоставляемый поиск — услуга. Классификация зависит от роли в ситуации.');} function society(){if(m===1||m===2||m===5){check('training','Сотрудники обучены');check('access','Есть доступный интерфейс');check('backup','Есть план при отказе');let time=S.training?5:14,errors=S.training?1:4,excluded=S.access?0:3;html=flow(['Подготовка','Цифровой процесс','Проверка результата'],2)+pad(stats([['Условное время',time+' мин'],['Ошибок в учебном сценарии',errors],['Участников без доступа',excluded],['При отказе',S.backup?'переход к резервному порядку':'работа останавливается']])+'

Числа заданы условиями вымышленного опыта. Это не статистика и не прогноз.

');msg='Установка техники сама по себе не решает вопросы навыков, доступности и непрерывности работы.';return} if(m===3){choices('Найден яркий пост без автора и даты. Как использовать утверждение из него?',['Сначала проверить первоисточник и подтверждения','Считать верным из-за популярности','Переслать без проверки'],0,'Проверьте автора, дату, доказательства и независимые подтверждения. Популярность не заменяет проверку.');return} choices('Сервис принимает заявления только онлайн. Часть людей не имеет устойчивой связи. Что улучшит доступ?',['Добавить доступный альтернативный канал','Увеличить число рекламных баннеров','Скрыть инструкцию за регистрацией'],0,'Альтернативный канал и понятный доступный интерфейс помогают уменьшить исключение людей из процесса.');} function education(){if(m===1||m===4||m===5){range('reading','Изучение примера, минут',0,60,5);range('practice','Самостоятельная практика, минут',0,60,5);range('review','Проверка и исправления, минут',0,30,5);let sum=S.reading+S.practice+S.review;html=flow(['Понять пример','Выполнить самостоятельно','Проверить','Исправить'],S.step%4)+pad(stats([['Всего времени',sum+' мин'],['Практика',S.practice?'запланирована':'отсутствует'],['Обратная связь',S.review?'есть время':'не предусмотрена']])+'

Проверяемая цель: сделать адаптивную карточку и показать её при ширине 360 и 1200 пикселей.

');next();msg=sum===0?'План пуст. Добавьте время на действия.':!S.practice?'Одно чтение не подтверждает освоение. Добавьте самостоятельную задачу.':!S.review?'Оставьте время на проверку и исправления.':'План содержит изучение, практику и проверку. Его длительность не гарантирует освоение — нужен работающий результат.';return} if(m===2){select('connection','Условия',[['slow','Связь нестабильная'],['live','Нужен общий разговор сейчас'],['flex','Участники учатся в разное время']]);let k={slow:0,live:1,flex:2}[S.connection];html=pad(stats([['Подходящий вариант',['Загружаемый текст + отложенные ответы','Синхронное занятие','Асинхронное задание'][k]]])+'

'+['Материалы доступны после загрузки; вопросы можно передать позже.','Общение происходит одновременно; важны связь и общее расписание.','Каждый выбирает время, но срок и критерии результата остаются ясными.'][k]+'

');msg='Формат выбирают по условиям и задаче. Дистанционное обучение может быть и синхронным, и асинхронным.';return} select('kind','Задача',[['','Написать и проверить обработчик'],['design','Спроектировать понятную форму'],['data','Найти закономерности в данных'],['security','Подготовить план восстановления']]);html=pad(stats([['Основной акцент',{'':'Разработка',design:'Дизайн взаимодействия',data:'Аналитика',security:'Безопасность'}[S.kind]]])+'

Доказательство навыка: '+{'':'работающий пример и проверки пограничных значений.',design:'макет и результаты проверки с пользователями.',data:'воспроизводимый расчёт и объяснение ограничений.',security:'проверенный порядок восстановления и разбор результата.'}[S.kind]+'

');msg='Роли пересекаются. Название профессии не заменяет знакомство с её задачами.';} function security(){if(m===1){select('kind','Происшествие',[['','Посторонний прочитал закрытые данные'],['edit','Запись изменена без разрешения'],['down','Сервис перестал отвечать']]);html=flow(['Конфиденциальность','Целостность','Доступность'],{'':0,edit:1,down:2}[S.kind])+pad('

Основная нарушенная цель: '+{'':'конфиденциальность',edit:'целостность',down:'доступность'}[S.kind]+'.

');msg='Один реальный инцидент может нарушать несколько целей. Здесь выделено прямое последствие условия.';return} if(m===2){choices('В программе есть известная неисправленная ошибка защиты. Как назвать это слабое место?',['Уязвимость','Уже произошедший инцидент','Резервная копия'],0,'Уязвимость — слабое место. Возможное воздействие — угроза; произошедшее нарушение — инцидент.');return} if(m===3){choices('Знакомый аккаунт просит срочно прислать код входа. Ваше действие?',['Не передавать код; проверить через известный контакт','Отправить код, имя выглядит знакомым','Перейти по присланной ссылке и ввести пароль'],0,'Имя и фото могут быть подменены. Проверяйте через заранее известный независимый канал; код входа не сообщают собеседнику.');return} check('backup','Создана резервная копия');check('independent','Копия отделена от рабочей среды');check('tested','Восстановление проверено');range('detect','Обнаружение и решение, часов',1,8);range('restore','Восстановление по плану, часов',1,12);let valid=S.backup&&S.independent,known=valid&&S.tested;html=flow(['Подготовиться','Сохранить важные функции','Восстановить','Улучшить'],S.step%4)+pad(stats([['Состояние копии',!S.backup?'нет':!S.independent?'зависит от той же среды':!S.tested?'не проверена':'проверена и отделена'],['Условный простой',known?S.detect+S.restore+' ч':'время не определено условиями']])+'

В учебном сценарии простой = обнаружение и решение + восстановление. В реальности сроки зависят от многих факторов; это не прогноз риска.

');next();msg=known?'После восстановления выясняют причину и улучшают защиту.':!valid?'Синхронизация в той же среде может распространить повреждение. Нужна независимая копия.':'Наличие копии не подтверждает возможность восстановления. Нужна проверка.';} function hints(){stage.querySelectorAll('.v9-scroll,.site-scroll,.box-scroll').forEach(el=>{if(el.scrollWidth>el.clientWidth+1){el.tabIndex=0;el.setAttribute('aria-label','Область с горизонтальной прокруткой');let h=document.createElement('div');h.className='v9-scroll-hint';h.textContent='← Можно прокручивать в стороны →';el.after(h)}})} function render(keep=false){const restoreFocus=retainWorkshopFocus(root);html='';ct='';msg='';[null,events,designer,controls,graphics,apps,webConcepts,editor,css,media,builder,site,systems,society,education,security][n]();stage.innerHTML=html;feed.textContent=msg;let frag=document.createElement('div');frag.innerHTML=ct;const signature=b=>[...b.querySelectorAll('input,select,textarea,button')].map(el=>[el.tagName,el.type,el.dataset.key||el.dataset.act].join(':')).join('|');if(keep&&signature(tools)===signature(frag)){tools.querySelectorAll('[data-key]').forEach(old=>{let nxt=frag.querySelector('[data-key="'+old.dataset.key+'"]'+(old.type==='radio'?'[value="'+old.value+'"]':''));let st=old.closest('label').querySelector('strong'),ns=nxt?.closest('label').querySelector('strong');if(st&&ns)st.textContent=ns.textContent;if(nxt&&document.activeElement!==old){if(old.type==='checkbox'||old.type==='radio')old.checked=nxt.checked;else old.value=nxt.value}})}else{let old=document.activeElement,k=tools.contains(old)?old.dataset.key:null,start=old?.selectionStart,end=old?.selectionEnd;tools.innerHTML=ct;if(keep&&k){let el=tools.querySelector('[data-key="'+k+'"]');el?.focus({preventScroll:true});if(start!=null)try{el.setSelectionRange(start,end)}catch(e){}}}hints();root.dataset.state=JSON.stringify(S);bindDrawing();restoreFocus();stage.querySelectorAll('audio,video').forEach(el=>el.addEventListener('error',()=>{feed.textContent='Браузер не смог воспроизвести формат. Текстовое описание записи приведено рядом.'}));} function stop(){clearInterval(timer);timer=null;S.running=false;stage.querySelectorAll('audio,video').forEach(e=>e.pause())} function tick(){S.position+=S.distance;if(S.position>620)S.position=20;let dot=stage.querySelector('[data-moving]');if(dot)dot.setAttribute('cx',S.position);root.dataset.state=JSON.stringify(S)} function record(s){S.log.push(s);if(S.log.length>8)S.log.shift()} function action(a){if(a==='focus'){let b=stage.querySelector('[data-act="click"]');if(b&&!b.disabled){b.focus();feed.textContent='Focus() перевёл фокус на кнопку.'}else feed.textContent='Скрытая или недоступная кнопка не принимает фокус.';return}if(a==='step')S.step++;else if(a.startsWith('answer'))S.answer=Number(a.slice(6));else if(a==='click'){if(n===6){S.count++;render();return}if(S.enabled){if(S.linked){S.count++;record('button1 · Click → значение '+S.count)}else record('Click: пользовательский обработчик не назначен')}}else if(a==='event'){if(S.event==='Click'){if(S.enabled&&S.visible){if(S.linked){S.count++;record('button1 · Click → '+S.count)}else record('Click: нет назначенного обработчика')}else record('Кнопка не доступна для активации')}else record((S.event==='Load'?'form1':'timer1')+' · '+S.event+' → запись в журнал')}else if(a==='zero'){S.count=0;record('resetButton · Click → 0')}else if(a==='greet')record(S.name.trim()?'Здравствуйте, '+S.name.trim()+'!':'Сначала введите имя.');else if(a==='play'){if(S.running)stop();else{S.running=true;timer=setInterval(tick,S.interval)}}else if(a==='tick')tick();else if(a==='dialog'){S.chosen=S.color;S.dialog=true}else if(a==='apply'){S.color=S.chosen;S.dialog=false}else if(a==='cancel')S.dialog=false;else if(a==='save')S.saved=S.draft;else if(a==='restore')S.draft=S.saved;else if(a==='undo')S.strokes.pop();else if(a==='clearDrawing')S.strokes=[];else if(a==='up'||a==='down'){let i=S.selected,j=i+(a==='up'?-1:1);if(j>=0&&j{let r=board.getBoundingClientRect();return[Math.max(0,Math.min(600,(e.clientX-r.left)/r.width*600)),Math.max(0,Math.min(260,(e.clientY-r.top)/r.height*260))]};board.addEventListener('pointerdown',e=>{if(e.button!==0)return;e.preventDefault();board.setPointerCapture(e.pointerId);drawing=[point(e)];drawing.push([drawing[0][0]+.1,drawing[0][1]+.1]);path=document.createElementNS('http://www.w3.org/2000/svg','polyline');path.setAttribute('fill','none');path.setAttribute('stroke','#356b49');path.setAttribute('stroke-width','5');path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('points',drawing.map(p=>p.join(',')).join(' '));board.append(path)});board.addEventListener('pointermove',e=>{if(!drawing)return;drawing.push(point(e));path.setAttribute('points',drawing.map(p=>p.join(',')).join(' '))});const end=()=>{if(!drawing)return;S.strokes.push(drawing);drawing=null;root.dataset.state=JSON.stringify(S)};board.addEventListener('pointerup',end);board.addEventListener('pointercancel',end)} root.addEventListener('click',e=>{let b=e.target.closest('[data-act]');if(b&&root.contains(b))action(b.dataset.act);let a=e.target.closest('.web-preview a');if(a){e.preventDefault();let id=a.getAttribute('href')?.slice(1),target=id?document.getElementById(id):null;target?.scrollIntoView({block:'nearest'})}}); root.addEventListener('input',e=>{if(e.target.hasAttribute('data-demo-input')){record('textBox1 · TextChanged → '+e.target.value);stage.querySelector('.event-output').textContent=S.log.at(-1);root.dataset.state=JSON.stringify(S)}}); function update(e){let el=e.target,k=el.dataset.key;if(!k)return;let v=el.type==='checkbox'?el.checked:el.value;if(['range','number'].includes(el.type)){if(v===''||!Number.isFinite(Number(v))||Number(v)Number(el.max)){feed.textContent='Введите число от '+el.min+' до '+el.max+'. Пока используется предыдущее значение.';return}v=Number(v)}else if((el.tagName==='SELECT'||el.type==='radio')&&typeof S[k]==='number')v=Number(v);if(S.running)stop();S[k]=v;S.answer=-1;render(el.tagName!=='SELECT'&&el.type!=='checkbox')} tools.addEventListener('input',e=>{if(e.target.tagName!=='SELECT'&&e.target.type!=='checkbox')update(e)});tools.addEventListener('change',e=>{if(e.target.type==='number'){let el=e.target,v=el.value===''?S[el.dataset.key]:Number(el.value);if(!Number.isFinite(v))v=S[el.dataset.key];let step=Number(el.step)||1;el.value=Number(Math.min(Number(el.max),Math.max(Number(el.min),Number(el.min)+Math.round((v-Number(el.min))/step)*step)).toFixed(6));update(e)}else if(e.target.tagName==='SELECT'||e.target.type==='checkbox')update(e)}); root.querySelector('.iv-reset').onclick=()=>{stop();S=structuredClone(base);render()};root.addEventListener('pause-diagram',()=>{if(S.running){stop();render()}});document.addEventListener('visibilitychange',()=>{if(document.hidden){stop();if(n===4)render()}});init();render(); });})();