# AlfaToad.CustomControls
Библиотека пользовательских контролов для **Windows Forms** (.NET Framework 4.8 и .NET 8).
Пакет: [NuGet — AlfaToad.CustomControls](https://www.nuget.org/packages/AlfaToad.CustomControls)
## Компоненты
| Контрол | Назначение |
|---------|------------|
| `ScrollableDetailPanel` | Прокручиваемый список строк с фото, полями товара и скидкой |
| `ScrollableOrderPanel` | Прокручиваемый список строк заказа |
| `BoundDataGridView` | Таблица с привязкой к `DataSet` и подсветкой столбца |
---
## Установка
```bash
dotnet add package AlfaToad.CustomControls --version 4.2.1
```
```xml
```
---
## ScrollableDetailPanel — настраиваемые свойства
### Поля данных (имена столбцов в `DataTable`)
| Поле на экране | Свойство | Значение по умолчанию |
|----------------|----------|----------------------|
| Фото | `ImageField` | `Фото` |
| Категория | `CategoryField` | `Категория товара` |
| Наименование | `NameField` | `Наименование товара` |
| Описание | `DescriptionField` | `Описание товара` |
| Производитель | `ManufacturerField` | `Производитель` |
| Поставщик | `SupplierField` | `Поставщик` |
| Цена | `PriceField` | `Цена` |
| Единица измерения | `UnitField` | `Единица измерения` |
| Количество на складе | `StockField` | `Кол-во на складе` |
| Скидка | `DiscountField` | `Действующая скидка` |
### Подписи на экране
| Подпись | Свойство | Значение по умолчанию |
|---------|----------|----------------------|
| Описание | `DescriptionCaption` | `Описание товара:` |
| Производитель | `ManufacturerCaption` | `Производитель:` |
| Поставщик | `SupplierCaption` | `Поставщик:` |
| Цена | `PriceCaption` | `Цена:` |
| Единица измерения | `UnitCaption` | `Единица измерения:` |
| Количество на складе | `StockCaption` | `Количество на складе:` |
| Заголовок столбца скидки | `DiscountCaption` | `Действующая скидка` |
### Изображения
| Свойство | Описание |
|----------|----------|
| `NoImage` | Картинка при отсутствии файла или пустом поле. По умолчанию **не задана** — область фото остаётся пустой. |
| `ImagesFolder` | Папка, из которой загружаются файлы по имени из `ImageField`. |
### Размеры строки
| Свойство | По умолчанию |
|----------|--------------|
| `RowHeight` | `160` |
| `ImageColumnWidth` | `120` |
| `DiscountColumnWidth` | `110` |
| `RowSpacing` | `8` (зона сепаратора между строками) |
| `OutOfStockRowColor` | голубой `#ADD8E6` (количество на складе ≤ 0) |
| `RowSeparatorColor` | `Silver` (линия между записями) |
| `ImageLeftPadding` | `8` (отступ фото от левого края) |
| `DataSource` | источник данных в дизайнере (DataSet / DataTable / BindingSource) |
| `DataMember` | имя таблицы в DataSet |
---
## ScrollableOrderPanel — настраиваемые свойства
### Поля данных
| Поле на экране | Свойство | Значение по умолчанию |
|----------------|----------|----------------------|
| Артикул заказа | `ArticleField` | `Артикул заказа` |
| Статус | `StatusField` | `Статус заказа` |
| Адрес пункта выдачи | `AddressField` | `Адрес пункта выдачи` |
| Дата заказа | `OrderDateField` | `Дата заказа` |
| Дата доставки | `DeliveryDateField` | `Дата доставки` |
### Подписи на экране
| Подпись | Свойство | Значение по умолчанию |
|---------|----------|----------------------|
| Артикул | `ArticleCaption` | `Артикул заказа` |
| Статус | `StatusCaption` | `Статус заказа` |
| Адрес | `AddressCaption` | `Адрес пункта выдачи` |
| Дата заказа | `OrderDateCaption` | `Дата заказа` |
| Дата доставки | `DeliveryDateCaption` | `Дата доставки` |
### Размеры строки
| Свойство | По умолчанию |
|----------|--------------|
| `RowHeight` | `110` |
| `DeliveryColumnWidth` | `140` |
---
## Добавление, редактирование и удаление (CRUD)
Оба списка (`ScrollableDetailPanel`, `ScrollableOrderPanel`) поддерживают кнопки **Добавить / Изменить / Удалить** и клик по строке для редактирования. Формы добавления и редактирования открывает **ваше приложение** в обработчиках событий.
### Права по ролям
| Свойство | Описание | По умолчанию |
|----------|----------|--------------|
| `AllowAdd` | Показать кнопку добавления | `false` |
| `AllowEdit` | Показать кнопку изменения и открывать редактирование по клику на строку | `false` |
| `AllowDelete` | Показать кнопку удаления | `false` |
| `IsEditSessionActive` | Блокирует второе окно редактирования, пока `true` | `false` |
| `ValidateBeforeDelete` | Функция проверки перед удалением; верните текст ошибки для запрета | `null` |
При `false` соответствующая кнопка **скрывается**. При попытке действия без прав показывается `MessageBox` с пояснением.
### События
| Событие | Когда срабатывает |
|---------|-------------------|
| `RecordAddRequested` | Нажата кнопка «Добавить» |
| `RecordEditRequested` | Клик по строке или кнопка «Изменить» |
| `RecordDeleting` | Перед удалением; установите `Cancel = true` и `DenyReason` для отмены |
| `RecordDeleted` | Строка удалена из `DataTable` (вызовите `Update` в БД) |
После закрытия формы редактирования вызовите `EndEditSession()`, чтобы снова можно было открыть другое окно.
### Пример для администратора (товары)
```csharp
var panel = new ScrollableDetailPanel { Dock = DockStyle.Fill };
panel.AllowAdd = panel.AllowEdit = panel.AllowDelete = userIsAdmin;
panel.ShowQueryPanel = userIsManagerOrAdmin;
panel.ValidateBeforeDelete = row =>
{
var article = row["Артикул"]?.ToString();
return ProductExistsInOrders(article)
? "Товар присутствует в заказе и не может быть удалён."
: null;
};
panel.RecordAddRequested += (_, __) => OpenProductForm(null);
panel.RecordEditRequested += (_, e) => OpenProductForm(e.Row);
panel.RecordDeleted += (_, e) =>
{
tableAdapter.Update(dataSet);
panel.RefreshRows();
};
```
### Пример для менеджера (заказы — только просмотр)
```csharp
ordersPanel.AllowAdd = ordersPanel.AllowEdit = ordersPanel.AllowDelete = userIsAdmin;
// менеджер: все три свойства false — кнопки скрыты, клик только выделяет строку
```
### Обработка исключительных случаев
Библиотека показывает `MessageBox` в ситуациях:
- действие недоступно для роли;
- не выбрана запись для изменения или удаления;
- уже открыто окно редактирования (`IsEditSessionActive`);
- `ValidateBeforeDelete` или `RecordDeleting` запретили удаление;
- ошибка при удалении строки из `DataTable`.
---
## BoundDataGridView — настраиваемые свойства
| Свойство | Описание | По умолчанию |
|----------|----------|--------------|
| `HighlightColumnName` | Столбец для подсветки ячеек | `Действующая скидка` |
| `ApplyRowHighlight` | Включить подсветку | `true` |
| `Appearance` | Палитра цветов | см. ниже |
---
## AppearancePalette — настройка цветов и шрифтов
Общий объект `Appearance` доступен у всех трёх контролов.
### Редактор в дизайнере Visual Studio
1. Выберите контрол на форме.
2. В окне **Свойства** найдите `Appearance`.
3. **Разверните** узел — каждый цвет редактируется стандартным выбором цвета.
4. Нажмите **«…»** рядом с `Appearance` — откроется окно **«Палитра оформления»** со всеми цветами, шрифтами и порогом скидки; кнопка **«По умолчанию»** сбрасывает значения.
Изменения сразу применяются к контролу на форме в режиме дизайна.
| Свойство | Назначение | По умолчанию |
|----------|------------|--------------|
| `MainBackground` | Основной фон | `#FFFFFF` |
| `SectionBackground` | Фон заголовков таблицы | `#F5DEB3` |
| `AccentBackground` | Линии сетки таблицы | `#DEB887` |
| `HighlightBackground` | Подсветка скидки > порога | `#FFDEAD` |
| `SelectedRowBackground` | Фон выбранной строки списка | `#E8EEF8` |
| `PrimaryText` | Цвет текста | `Black` |
| `SeparatorText` | Цвет разделителя `\|` | `Gray` |
| `BorderColor` | Рамки строк списка | `Black` |
| `RowSeparatorColor` | Линия между записями | `Silver` |
| `OutOfStockBackground` | Фон строки без товара на складе | `#ADD8E6` |
| `StrikethroughPriceColor` | Перечёркнутая цена при скидке | `Red` |
| `FinalPriceColor` | Итоговая цена при скидке | `Black` |
| `FontFamily` | Шрифт | `Arial` |
| `BaseFontSize` | Размер обычного текста | `9` |
| `TitleFontSize` | Размер заголовка строки | `10` |
| `HighlightThresholdPercent` | Порог подсветки скидки (%) | `17` |
---
## Алгоритм расчёта скидки
### Назначение
Вычисление **итоговой цены товара** с учётом действующей скидки и определение **способа отображения** цены на экране (`ScrollableDetailPanel`, `BoundDataGridView`).
### Входные данные
| Обозначение | Описание | Источник |
|-------------|----------|----------|
| `Ц` | Исходная цена товара | столбец `Цена` (`PriceField`) |
| `С` | Действующая скидка, % | столбец `Действующая скидка` (`DiscountField`) |
| `П` | Порог подсветки скидки, % | `Appearance.HighlightThresholdPercent` (по умолчанию 17) |
### Выходные данные
| Обозначение | Описание |
|-------------|----------|
| `Ц_итог` | Итоговая цена (руб., 2 знака после запятой) |
| `Режим` | Способ отображения: «только цена» или «перечёркнутая + итоговая» |
| `Подсветка` | Применять ли фон подсветки для ячейки/блока скидки |
### Формула
Если `Ц > 0` и `С > 0`:
```
Ц_итог = ОКРУГЛ( Ц × (1 − С / 100) ; 2 )
```
Иначе:
```
Ц_итог = Ц
```
Округление — **математическое**, до 2 знаков (`MidpointRounding.AwayFromZero`).
### Обозначения блок-схемы (ГОСТ 19.701-90)
| Символ | Наименование по ГОСТ | Назначение в схеме |
|--------|----------------------|--------------------|
| ◯ (овал) | **Терминатор** | Начало / конец алгоритма |
| ▱ (параллелограмм) | **Данные** | Ввод исходных значений `Ц`, `С`, `П` |
| ▭ (прямоугольник) | **Процесс** | Вычисление, присваивание, форматирование |
| ◇ (ромб) | **Решение** | Проверка условия (да / нет) |
### Блок-схема алгоритма (ГОСТ 19.701-90)
В диаграмме Mermaid: овал — терминатор, параллелограмм — ввод/вывод, прямоугольник — процесс, ромб — решение.
```mermaid
flowchart TD
A([НАЧАЛО]) --> B[/Ввод: Ц — цена, С — скидка %, П — порог подсветки/]
B --> C{Ц ≤ 0
ИЛИ
С ≤ 0?}
C -- ДА --> D[Ц_итог := Ц]
C -- НЕТ --> E[Ц_итог := ОКРУГЛ Ц × 1 − С / 100 ; 2]
D --> F{С > 0
И
Ц > 0?}
E --> F
F -- ДА --> G[Режим := перечёркнутая Ц + итоговая Ц_итог]
F -- НЕТ --> H[Режим := отображать только Ц]
G --> I{С > П?}
H --> I
I -- ДА --> J[Подсветка := включить HighlightBackground]
I -- НЕТ --> K[Подсветка := обычный фон]
J --> L[/Вывод: Ц_итог, Режим, Подсветка/]
K --> L
L --> M([КОНЕЦ])
```
### Блок-схема в текстовом виде (ГОСТ 19.701-90)
```
┌─────────────────────────────────┐
│ НАЧАЛО │ ← терминатор (овал)
└───────────────┬─────────────────┘
▼
╔═════════════════════════════════╗
║ Ввод: Ц, С, П ║ ← данные (параллелограмм)
╚═════════════════════════════════╝
▼
┌───────────────┐
│ Ц ≤ 0 ИЛИ │
│ С ≤ 0 ? │ ← решение (ромб)
└───┬───────┬───┘
ДА │ │ НЕТ
▼ ▼
┌───────────────┐ ┌───────────────────────────────┐
│ Ц_итог := Ц │ │ Ц_итог := ОКРУГЛ(Ц×(1−С/100);2)│ ← процесс
└───────┬───────┘ └───────────────┬───────────────┘
└───────────┬───────────────┘
▼
┌───────────────┐
│ С > 0 И │
│ Ц > 0 ? │ ← решение (ромб)
└───┬───────┬───┘
ДА │ │ НЕТ
▼ ▼
┌───────────────────────┐ ┌─────────────────────────┐
│ Режим := перечёркнутая│ │ Режим := только Ц │
│ цена + Ц_итог │ └───────────┬─────────────┘
└───────────┬───────────┘ │
└───────────┬───────────────┘
▼
┌───────────────┐
│ С > П ? │ ← решение (ромб)
└───┬───────┬───┘
ДА │ │ НЕТ
▼ ▼
┌───────────────────────┐ ┌─────────────────────────┐
│ Подсветка := ДА │ │ Подсветка := НЕТ │
└───────────┬───────────┘ └───────────┬─────────────┘
└───────────┬───────────────┘
▼
╔═════════════════════════════════╗
║ Вывод: Ц_итог, Режим, ║ ← данные (параллелограмм)
║ Подсветка ║
╚═════════════════════════════════╝
▼
┌─────────────────────────────────┐
│ КОНЕЦ │ ← терминатор (овал)
└─────────────────────────────────┘
```
### Таблица решений (ветвления)
| № | Условие | Действие |
|---|---------|----------|
| 1 | `Ц ≤ 0` **или** `С ≤ 0` | `Ц_итог = Ц` (скидка не применяется) |
| 2 | `Ц > 0` **и** `С > 0` | `Ц_итог = ОКРУГЛ(Ц × (1 − С/100); 2)` |
| 3 | `С > 0` **и** `Ц > 0` | Показать старую цену **красной перечёркнутой** и рядом **итоговую чёрную** |
| 4 | иначе | Показать только `Ц` |
| 5 | `С > П` | Подсветить ячейку/блок скидки цветом `HighlightBackground` |
| 6 | `С ≤ П` | Обычный фон |
### Примеры расчёта
| Ц, руб. | С, % | Ц_итог, руб. | Отображение |
|---------|------|--------------|-------------|
| 1000 | 0 | 1000,00 | `1000,00` |
| 1000 | 15 | 850,00 | ~~1000,00~~ **850,00** |
| 1000 | 20 | 800,00 | ~~1000,00~~ **800,00** + подсветка (20 > 17) |
| 0 | 10 | 0,00 | `0,00` (скидка не считается) |
| 500 | −5 | 500,00 | `500,00` (отрицательная скидка игнорируется) |
### Соответствие коду библиотеки
| Шаг алгоритма | Реализация |
|---------------|------------|
| Расчёт `Ц_итог` | `ScrollableDetailPanel.CalculateFinalPrice` |
| Отображение цены в строке | `ProductListRow` → `PriceLinePanel` |
| Подсветка скидки в списке | `Appearance.HighlightThresholdPercent` |
| Подсветка в таблице | `BoundDataGridView.OnCellFormatting` |
---
## Примеры использования
### Список товаров с привязкой к DataSet
```csharp
using System.Data;
using System.Drawing;
using AlfaToad.CustomControls.Controls;
using AlfaToad.CustomControls.Theme;
var panel = new ScrollableDetailPanel { Dock = DockStyle.Fill };
panel.NoImage = Image.FromFile(@"C:\images\no-photo.png");
panel.ImagesFolder = @"C:\images\products";
panel.CategoryField = "CategoryName";
panel.NameField = "ProductName";
panel.DiscountField = "DiscountPercent";
panel.Appearance = new AppearancePalette
{
MainBackground = Color.White,
HighlightBackground = Color.FromArgb(255, 222, 173),
BorderColor = Color.Black,
HighlightThresholdPercent = 17m
};
DataSet dataSet = LoadProducts();
panel.BindDataSet(dataSet, "Products");
```
**Поведение цены:** если `Действующая скидка` > 0, основная цена отображается красной и перечёркнутой, рядом — итоговая цена чёрным (`цена × (1 − скидка/100)`).
**Нет на складе:** если `Кол-во на складе` ≤ 0, вся строка подсвечивается цветом `OutOfStockRowColor`.
### Поиск, фильтрация и сортировка (`ScrollableDetailPanel`)
Панель включена по умолчанию (`ShowQueryPanel = true`). Всё применяется **в реальном времени** без кнопки «Найти».
| Элемент | Поведение |
|---------|-----------|
| **Поиск** | Подстрока по всем текстовым столбцам (`string`), одновременно по нескольким полям (логика OR) |
| **Сортировка** | Цена ↑/↓, кол-во на складе ↑/↓; выбор сохраняется при поиске и фильтре |
| **Поставщик** | Выпадающий список; первый пункт `Все поставщики` сбрасывает фильтр |
| Свойство | Описание |
|----------|----------|
| `ShowQueryPanel` | Показать панель (по умолчанию `true`) |
| `AllSuppliersCaption` | Текст сброса фильтра (`Все поставщики`) |
| `SearchFields` | Ограничить столбцы поиска (пусто = все текстовые) |
| `SupplierField` | Столбец для фильтра поставщика (`Поставщик`) |
| `PriceField` / `StockField` | Столбцы для сортировки |
```csharp
panel.ShowQueryPanel = userIsManagerOrAdmin;
```
После `TableAdapter.Fill(...)` список и список поставщиков обновляются автоматически.
### Список заказов
```csharp
var orders = new ScrollableOrderPanel { Dock = DockStyle.Fill };
orders.Appearance = AppearancePalette.CreateDefault();
orders.Appearance.AccentBackground = Color.BurlyWood;
orders.BindDataSet(dataSet, "Orders");
```
### Таблица с подсветкой скидки
```csharp
var grid = new BoundDataGridView { Dock = DockStyle.Fill };
grid.Appearance = AppearancePalette.CreateDefault();
grid.HighlightColumnName = "Действующая скидка";
grid.ApplyDefaultStyle();
grid.BindDataSet(dataSet, "Products");
```
### Смена цветов после создания
```csharp
panel.Appearance.MainBackground = ColorTranslator.FromHtml("#FAFAFA");
panel.Appearance.HighlightBackground = ColorTranslator.FromHtml("#FFDEAD");
panel.Appearance.BorderColor = Color.DimGray;
panel.ApplyAppearance();
```