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

§ 1. Объектно-событийная модель работы программы

Разберём программу как систему объектов, которые хранят состояние и реагируют на события. Будем нажимать кнопки, менять свойства и читать журнал обработчиков.

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

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

1

Графический интерфейс и компоненты

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

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

Графический интерфейс и компоненты

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

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

Визуальный конструктор позволяет размещать компоненты и менять их свойства. Среда создаёт часть служебного кода, но алгоритм и обработку ошибок проектирует разработчик. В PascalABC.NET оконные примеры используют Windows Forms; VCL — другая библиотека, связанная с Delphi. Эти названия нельзя считать общим именем всех визуальных компонентов.

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

Объект, свойство и метод

Свойство описывает состояние объекта, метод выполняет действие над ним.

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

Объект, свойство и метод

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

У кнопки есть имя для кода, надпись, размеры, положение, видимость и доступность. Text задаёт отображаемый текст, Name — внутреннее имя, Enabled — возможность взаимодействия, Visible — видимость. Изменение Text не переименовывает объект в программе.

Метод — операция, доступная у объекта: например, Focus() запрашивает фокус, а Close() закрывает форму. Свойству присваивают значение, метод вызывают. Запись label1.Text := 'Готово' изменяет надпись. Вызов формы Close() меняет ход работы приложения. Набор свойств и методов определяется классом объекта.

КатегорияПримерСмысл
Свойствоbutton1.TextНадпись
МетодtextBox1.Focus()Передать фокус
Событиеbutton1.ClickАктивация кнопки
Главная мысль: Свойство описывает состояние объекта, метод выполняет действие над ним.
3

Событие и обработчик

Событие сообщает о произошедшем, а обработчик задаёт реакцию программы.

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

Событие и обработчик

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

Click, TextChanged, Load, Paint и Tick обозначают разные события. Click может возникнуть при активации кнопки мышью или клавиатурой; его нельзя свести исключительно к нажатию левой кнопки мыши. События поступают также от системы и таймера, без непосредственного действия человека.

Обработчик — подпрограмма, связанная с событием. В параметре sender обычно передаётся источник, в параметре e — сведения о событии. Имя обработчика само по себе не подключает его к кнопке: нужна связь в дизайнере или коде. Если пользовательский обработчик не назначен, приложение не обязано выдавать ошибку; может выполниться стандартное действие или ничего дополнительного.

{ Фрагмент обработчика формы в PascalABC.NET.
  label1 — уже созданная надпись; связь Click назначена. }
procedure MainForm.button1_Click(sender: object; e: EventArgs);
begin
  label1.Text := 'Готово';
end;
Главная мысль: Событие сообщает о произошедшем, а обработчик задаёт реакцию программы.
4

Состояние и последовательность событий

Результат зависит от текущего состояния и порядка вызова обработчиков.

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

Состояние и последовательность событий

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

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

Журнал событий помогает увидеть источник, событие и изменения данных. Обработчик должен завершаться достаточно быстро, чтобы интерфейс продолжал отвечать. Длительная работа в потоке интерфейса может задерживать другие события. Проверяют не только одиночное нажатие, но и повторные действия, выключенный компонент и изменение данных перед запуском команды.

Главная мысль: Результат зависит от текущего состояния и порядка вызова обработчиков.
Интерактивная практика

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

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

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

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

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

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

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

01

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

02

Свойство описывает состояние объекта, метод выполняет действие над ним.

03

Событие сообщает о произошедшем, а обработчик задаёт реакцию программы.

04

Результат зависит от текущего состояния и порядка вызова обработчиков.

05

Связь события с обработчиком важнее имени самой процедуры.

06

Повторные действия и недоступные элементы входят в проверку интерфейса.

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

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

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

1. Для чего нужна форма?

2. Что задаёт Text кнопки?

3. Что обозначает Name?

4. Что является методом?

5. Что является событием?

6. Что такое обработчик?

7. Обязательно ли отсутствие своего обработчика вызывает ошибку?

8. Что обычно передаёт sender?

9. Click возможен только от мыши?

10. Итог команд +1, +1, сброс, +1 от нуля?

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

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

\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(); });})();