Release v1.0.21: consignees from web, remove test pharmacies.

This commit is contained in:
Exest 2026-08-05 15:22:44 +03:00
parent 166a431bda
commit 15dcf70bae
38 changed files with 1471 additions and 371 deletions

5
.gitignore vendored
View File

@ -20,3 +20,8 @@ Desktop.ini
efClient.db
*.db-journal
**/user.config
packages/
installer/output/
dist/

View File

@ -0,0 +1,110 @@
; ЭльФиСА — установщик «Электронная Фармация»
; Кодировка: UTF-8 with BOM
#define MyAppName "Электронная Фармация"
#define MyAppBrand "ЭльФиСА"
#define MyAppVersion "1.0.20"
#define MyAppPublisher "PharmData"
#define MyAppURL "https://cdn.24pharmdata.ru"
#define MyAppSupportURL "https://24pharmdata.ru"
#define MyAppExeName "Электронная Фармация.exe"
#define MyAppId "{{A7C4E2B1-9F3D-4E6A-8B2C-1D5F7A9E0C42}"
[Setup]
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppBrand} — {#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppSupportURL}
AppSupportURL={#MyAppSupportURL}
AppUpdatesURL={#MyAppURL}
; Программа — в LocalAppData (без прав админа). База/логи — тоже в профиле пользователя.
DefaultDirName={localappdata}\{#MyAppBrand}\{#MyAppName}
DefaultGroupName={#MyAppBrand}
DisableProgramGroupPage=no
LicenseFile=license.txt
InfoBeforeFile=welcome.txt
OutputDir=output
OutputBaseFilename=ElfisaPharmacy-Setup-{#MyAppVersion}
SetupIconFile=assets\elfisa.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppBrand} — {#MyAppName}
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
WizardSizePercent=120
WizardImageFile=assets\wizard-side.bmp
WizardSmallImageFile=assets\wizard-small.bmp
WizardImageStretch=yes
AllowNoIcons=yes
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
ArchitecturesInstallIn64BitMode=x64compatible
VersionInfoVersion={#MyAppVersion}.0
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription=Установщик {#MyAppBrand} — {#MyAppName}
VersionInfoProductName={#MyAppName}
VersionInfoProductVersion={#MyAppVersion}
VersionInfoCopyright=Copyright (C) {#MyAppPublisher}
ShowLanguageDialog=no
DisableWelcomePage=no
DisableDirPage=no
DisableFinishedPage=no
CloseApplications=yes
RestartApplications=no
SetupLogging=yes
[Languages]
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
[Messages]
russian.BeveledLabel={#MyAppBrand} · PharmData
russian.WelcomeLabel1=Добро пожаловать в установку [name]
russian.WelcomeLabel2=Мастер установит [name/ver] на ваш компьютер.%n%nРекомендуется закрыть все другие приложения перед продолжением.%n%nНажмите «Далее», чтобы продолжить.
russian.FinishedLabel=Установка [name] успешно завершена.%n%nПриложение готово к работе. При первом запуске выполните вход в систему.
russian.ClickFinish=Нажмите «Готово», чтобы выйти из мастера установки.
russian.SelectTasksDesc=Выберите дополнительные задачи, затем нажмите «Далее».
russian.SelectTasksLabel2=Какие дополнительные задачи нужно выполнить?
russian.WizardSelectTasks=Дополнительные задачи
russian.WizardReady=Всё готово к установке
russian.ReadyLabel1=Мастер готов установить [name] на ваш компьютер.
russian.ReadyLabel2a=Нажмите «Установить», чтобы продолжить, или «Назад», если хотите просмотреть либо изменить параметры.
russian.InstallingLabel=Подождите, пока [name] устанавливается на ваш компьютер…
russian.StatusExtractFiles=Распаковка файлов…
russian.StatusCreateDirs=Создание папок…
russian.StatusRegisterFiles=Регистрация компонентов…
russian.ConfirmUninstall=Вы действительно хотите удалить %1 и все его компоненты?
russian.UninstalledAll=%1 успешно удалена.
russian.ExitSetupMessage=Установка не завершена. Если вы выйдете сейчас, программа не будет установлена.%n%nВы можете запустить установщик позже.%n%nВыйти из установки?
[CustomMessages]
russian.CreateDesktopIcon=Создать ярлык на рабочем столе
russian.LaunchAfterInstall=Запустить Электронную Фармацию
russian.AppWorkingDir=Рабочая папка программы
russian.AdditionalIcons=Дополнительные ярлыки:
russian.BrandLine=ЭльФиСА — удобный заказ лекарств для аптек
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
; Основной выпуск Release (без отладочных PDB и XML-документации)
Source: "..\src\ElectronicPharmacy\bin\Release\*.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\src\ElectronicPharmacy\bin\Release\*.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\src\ElectronicPharmacy\bin\Release\*.config"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\src\ElectronicPharmacy\bin\Release\x86\*"; DestDir: "{app}\x86"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist
Source: "..\src\ElectronicPharmacy\bin\Release\x64\*"; DestDir: "{app}\x64"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist
Source: "assets\elfisa.ico"; DestDir: "{app}"; DestName: "elfisa.ico"; Flags: ignoreversion
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\elfisa.ico"; Comment: "{cm:BrandLine}"
Name: "{group}\Удалить {#MyAppName}"; Filename: "{uninstallexe}"; IconFilename: "{app}\elfisa.ico"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\elfisa.ico"; Comment: "{cm:BrandLine}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchAfterInstall}"; Flags: nowait postinstall skipifsilent
[UninstallDelete]
Type: filesandordirs; Name: "{app}\Logs"
Type: files; Name: "{app}\*.pdb"

24
installer/README.md Normal file
View File

@ -0,0 +1,24 @@
# Установщик ЭльФиСА
## Публичная ссылка (CDN)
https://cdn.24pharmdata.ru
При открытии корня CDN сразу скачивается установщик.
Стабильное имя файла:
https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe
## Локальная сборка
- `installer/output/ElfisaPharmacy-Setup-1.0.20.exe`
- `installer/output/ElfisaPharmacy-Setup.exe` (копия для CDN)
## Пересборка
```powershell
.\scripts\build-installer.ps1
```
После сборки залейте `ElfisaPharmacy-Setup.exe` на CDN (или сделайте его index/default document).

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
installer/assets/elfisa.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

18
installer/license.txt Normal file
View File

@ -0,0 +1,18 @@
ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ
Электронная Фармация (ЭльФиСА)
1. Программа предоставляется для использования в рамках договора
с поставщиком услуг PharmData / 24pharmdata.
2. Запрещается передавать третьим лицам учётные данные доступа,
а также модифицировать клиент способом, нарушающим безопасность
обмена данными.
3. Пользователь несёт ответственность за корректность оформляемых
заказов и за сохранность локальных данных на рабочем месте.
4. Устанавливая программу, вы подтверждаете согласие с условиями
использования сервиса и политикой обработки данных, действующими
у оператора сервиса.
При возникновении вопросов обратитесь в службу поддержки.

20
installer/welcome.txt Normal file
View File

@ -0,0 +1,20 @@
ЭльФиСА — Электронная Фармация
================================
Добро пожаловать в установку настольного клиента «Электронная Фармация».
Программа предназначена для работы аптек с прайс-листами поставщиков,
заказами, накладными и обменом данными с сервером PharmData.
Скачать установщик всегда можно по адресу:
https://cdn.24pharmdata.ru
Установщик:
• скопирует программу в выбранную папку;
• предложит ярлык в меню «Пуск»;
• по желанию создаст ярлык на рабочем столе.
Рекомендуется закрыть запущенную «Электронную Фармацию» перед установкой.
После установки при первом запуске выполните вход в систему.
© PharmData / ЭльФиСА

View File

@ -0,0 +1,48 @@
#Requires -Version 5.1
param([switch]$SkipBuild)
$ErrorActionPreference = "Stop"
$root = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $root
Write-Host "Root: $root" -ForegroundColor Cyan
$iscc = @(
"$env:LocalAppData\Programs\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles}\Inno Setup 6\ISCC.exe"
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) {
throw "Inno Setup 6 (ISCC.exe) не найден. Установите: winget install JRSoftware.InnoSetup"
}
if (-not $SkipBuild) {
Write-Host "Сборка Release..." -ForegroundColor Cyan
dotnet build "src\ElectronicPharmacy\ElectronicPharmacy.csproj" -c Release
if ($LASTEXITCODE -ne 0) { throw "Сборка приложения завершилась с ошибкой" }
}
Write-Host "Сборка установщика..." -ForegroundColor Cyan
& $iscc "installer\ElfisaPharmacy.iss"
if ($LASTEXITCODE -ne 0) { throw "Inno Setup завершился с ошибкой" }
$out = Get-ChildItem "installer\output\ElfisaPharmacy-Setup-*.exe" |
Where-Object { $_.Name -notmatch '\.bak\.' -and $_.Name -ne 'ElfisaPharmacy-Setup.exe' } |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $out) {
throw "Не найден собранный Setup exe в installer\output"
}
# Стабильное имя для CDN: https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe
$stable = Join-Path $out.DirectoryName "ElfisaPharmacy-Setup.exe"
Copy-Item $out.FullName $stable -Force
Write-Host ""
Write-Host "Готово: $($out.FullName)" -ForegroundColor Green
Write-Host "CDN-файл: $stable" -ForegroundColor Green
Write-Host ("Размер: {0:N1} МБ" -f ($out.Length / 1MB))
Write-Host ""
Write-Host "Залейте на CDN:" -ForegroundColor Cyan
Write-Host " https://cdn.24pharmdata.ru/ <- корень отдаёт этот Setup"
Write-Host " или файл: ElfisaPharmacy-Setup.exe"

View File

@ -374,6 +374,30 @@ namespace Электронная_Фармация.Classes
}
}
public async Task<List<BuyerLocationDto>> GetBuyerLocationsAsync()
{
EnsureAuthenticated();
var responseBody = await SendAuthorizedGetAsync("/api/buyer/locations", "загрузку грузополучателей");
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<BuyerLocationDto>>(responseBody, JsonOptions)
?? new List<BuyerLocationDto>();
}
}
return new List<BuyerLocationDto>();
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать грузополучателей", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке грузополучателей: {ex.Message}", ex);
}
}
public async Task<BuyerOrderDetailDto> GetBuyerOrderByIdAsync(string orderId)
{
EnsureAuthenticated();
@ -734,6 +758,27 @@ namespace Электронная_Фармация.Classes
public List<BuyerOrderListItemDto> Orders { get; set; }
}
public sealed class BuyerLocationDto
{
[JsonPropertyName("buyer_location_id")]
public string BuyerLocationID { get; set; }
[JsonPropertyName("buyer_id")]
public string BuyerID { get; set; }
[JsonPropertyName("address")]
public string Address { get; set; }
[JsonPropertyName("region_id")]
public string RegionID { get; set; }
[JsonPropertyName("is_default")]
public bool IsDefault { get; set; }
[JsonPropertyName("region_name")]
public string RegionName { get; set; }
}
public sealed class BuyerOrderListItemDto
{
[JsonPropertyName("order_id")]

View File

@ -12,11 +12,42 @@ namespace Электронная_Фармация.Classes
public const string DefaultApiBaseUrl = "https://24pharmdata.ru";
public const string DefaultUpdateCheckBaseUrl = "https://git.24pharmdata.ru";
public const string DefaultUpdateRepo = "pharmdata/elfisa-pharmacy";
/// <summary>Публичная раздача установщика (корень CDN отдаёт Setup.exe).</summary>
public const string DefaultInstallerCdnUrl = "https://cdn.24pharmdata.ru";
public const string DefaultInstallerDownloadUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
private const string LegacyApiBaseUrl = "http://195.34.241.84:9988";
private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL";
private static bool _sqliteInteropPrepared;
private static readonly object _sqliteInteropLock = new object();
public static string InstallerDownloadUrl
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_INSTALLER_URL");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return fromEnv.Trim();
}
return DefaultInstallerDownloadUrl;
}
}
public static string InstallerCdnUrl
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_CDN_URL");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return fromEnv.Trim().TrimEnd('/');
}
return DefaultInstallerCdnUrl;
}
}
/// <summary>
/// Replaces known dead API URLs in saved user settings.
/// </summary>
@ -97,24 +128,77 @@ namespace Электронная_Фармация.Classes
return url;
}
/// <summary>
/// Writable per-user data (DB, logs). Program Files is read-only for normal users.
/// </summary>
public static string UserDataDirectory
{
get
{
var dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ЭльФиСА",
"Электронная Фармация");
try
{
Directory.CreateDirectory(dir);
}
catch
{
// Caller will surface a clearer error when opening the DB.
}
return dir;
}
}
public static string LogsDirectory => Path.Combine(UserDataDirectory, "Logs");
public static string SqliteDbPath
{
get
{
MigrateLegacySqliteDatabaseIfNeeded();
return Path.Combine(UserDataDirectory, "efClient.db");
}
}
/// <summary>
/// One-time copy from install folder (old portable layout) into LocalAppData.
/// </summary>
private static void MigrateLegacySqliteDatabaseIfNeeded()
{
try
{
var target = Path.Combine(UserDataDirectory, "efClient.db");
if (File.Exists(target))
{
return;
}
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
var root = Path.Combine(baseDir, "efClient.db");
var data = Path.Combine(baseDir, "Data", "efClient.db");
if (File.Exists(root))
var candidates = new[]
{
return root;
}
Path.Combine(baseDir, "efClient.db"),
Path.Combine(baseDir, "Data", "efClient.db")
};
if (File.Exists(data))
foreach (var source in candidates)
{
return data;
}
if (!File.Exists(source))
{
continue;
}
return root;
File.Copy(source, target, overwrite: false);
AppDebugLog.Info("AppConfig", $"SQLite DB migrated: {source} -> {target}");
return;
}
}
catch (Exception ex)
{
AppDebugLog.Info("AppConfig", $"SQLite DB migrate skipped: {ex.Message}");
}
}

View File

@ -8,7 +8,7 @@ namespace Электронная_Фармация.Classes
{
/// <summary>
/// File logger for API/auth/network debugging.
/// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable.
/// Logs are written to %LocalAppData%\ЭльФиСА\...\Logs\api-debug-YYYY-MM-DD.log.
/// </summary>
public static class AppDebugLog
{
@ -33,8 +33,7 @@ namespace Электронная_Фармация.Classes
}
}
public static string LogDirectory =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
public static string LogDirectory => AppConfig.LogsDirectory;
public static string CurrentLogFilePath =>
Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log");

View File

@ -322,6 +322,131 @@ namespace Электронная_Фармация.Classes
}
}
/// <summary>
/// Подтягивает грузополучателей с сервера (BuyerLocation) в локальную таблицу Consignees.
/// Источник истины — веб-кабинет; локально только кэш + выбор активной аптеки.
/// </summary>
public static int SyncFromServer(IList<BuyerLocationDto> locations)
{
if (locations == null || locations.Count == 0)
{
return 0;
}
var upserted = 0;
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var tx = connection.BeginTransaction())
{
// Сбрасываем default — выставим по данным сервера.
using (var clearDefault = new SQLiteCommand(
"UPDATE Consignees SET ConsigneesUseAsDefault = 0",
connection,
tx))
{
clearDefault.ExecuteNonQuery();
}
int nextCode;
using (var nextCodeCmd = new SQLiteCommand(
"SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees",
connection,
tx))
{
nextCode = Convert.ToInt32(nextCodeCmd.ExecuteScalar());
}
foreach (var loc in locations)
{
var locationId = (loc.BuyerLocationID ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
continue;
}
var address = (loc.Address ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(address))
{
address = "Грузополучатель";
}
var name = address;
if (name.Length > 120)
{
name = name.Substring(0, 120);
}
long? existingId = null;
using (var find = new SQLiteCommand(
@"SELECT idConsignees FROM Consignees
WHERE lower(trim(LocationId)) = lower(@loc)
LIMIT 1",
connection,
tx))
{
find.Parameters.AddWithValue("@loc", locationId);
var found = find.ExecuteScalar();
if (found != null && found != DBNull.Value)
{
existingId = Convert.ToInt64(found);
}
}
if (existingId.HasValue)
{
using (var upd = new SQLiteCommand(
@"UPDATE Consignees
SET ConsigneesName = @name,
ConsigneesAddress = @address,
ConsigneesUseAsDefault = @isDefault,
LocationId = @loc
WHERE idConsignees = @id",
connection,
tx))
{
upd.Parameters.AddWithValue("@name", name);
upd.Parameters.AddWithValue("@address", address);
upd.Parameters.AddWithValue("@isDefault", loc.IsDefault ? 1 : 0);
upd.Parameters.AddWithValue("@loc", locationId);
upd.Parameters.AddWithValue("@id", existingId.Value);
upd.ExecuteNonQuery();
}
}
else
{
using (var ins = new SQLiteCommand(
@"INSERT INTO Consignees
(codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId)
VALUES (@code, @name, @address, @isDefault, @loc)",
connection,
tx))
{
ins.Parameters.AddWithValue("@code", nextCode++);
ins.Parameters.AddWithValue("@name", name);
ins.Parameters.AddWithValue("@address", address);
ins.Parameters.AddWithValue("@isDefault", loc.IsDefault ? 1 : 0);
ins.Parameters.AddWithValue("@loc", locationId);
ins.ExecuteNonQuery();
}
}
upserted++;
}
tx.Commit();
}
}
var active = GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(active))
{
SetActiveConsignee(active);
}
return upserted;
}
/// <summary>
/// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки.
/// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется

View File

@ -125,15 +125,36 @@ namespace Электронная_Фармация.Classes
}
else if (control is Panel || control is UserControl || control is TableLayoutPanel)
{
if (control.Tag as string == "keep-bg")
var tag = control.Tag as string;
if (tag == "keep-bg")
{
// preserve custom background
}
else if (control.Tag as string == "chrome-child")
else if (tag == "chrome-child")
{
control.BackColor = theme.BackgroundPrimary;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "toolbar")
{
control.BackColor = theme.Surface;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "status-footer")
{
control.BackColor = theme.CartHeaderBackground;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "surface-bar")
{
control.BackColor = theme.SectionHeaderBackground;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "markup-bar")
{
control.BackColor = theme.AccentPrimarySoft;
control.ForeColor = theme.TextPrimary;
}
else
{
control.BackColor = theme.BackgroundSecondary;

View File

@ -17,6 +17,7 @@
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<ApplicationIcon>Resources\elfisa.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@ -353,4 +354,7 @@
<Import Project="packages\EntityFramework.6.4.4\build\EntityFramework.targets" Condition="Exists('packages\EntityFramework.6.4.4\build\EntityFramework.targets')" />
<Import Project="packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets" Condition="Exists('packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets')" />
<Import Project="packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets" Condition="Exists('packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets')" />
<ItemGroup>
<None Include="Resources\elfisa.ico" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
namespace Электроннаяармация
namespace Электроннаяармация
{
partial class ElectroPharmacy
{
@ -381,8 +381,11 @@
this.ClientSize = new System.Drawing.Size(1089, 554);
this.Controls.Add(this.bindControl);
this.Controls.Add(this.tabContent);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
this.MaximizeBox = true;
this.MinimizeBox = true;
this.Margin = new System.Windows.Forms.Padding(4);
this.MinimumSize = new System.Drawing.Size(900, 620);
this.Name = "ElectroPharmacy";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Электронная Фармация";

View File

@ -23,6 +23,7 @@ namespace Электронная_Фармация
private ToastService _toastService;
private LoadingOverlay _loadingOverlay;
private ModernAppHeader _appHeader;
private ModernTitleBar _titleBar;
private ModernDocumentTabStrip _documentTabStrip;
private Panel _documentHost;
private Panel _statusFooter;
@ -38,6 +39,9 @@ namespace Электронная_Фармация
Font = new Font("Segoe UI", 12f);
AutoScaleMode = AutoScaleMode.Dpi;
KeyPreview = true;
FormBorderStyle = FormBorderStyle.None;
MaximizeBox = true;
MinimizeBox = true;
MinimumSize = new Size(900, 620);
bindControl.Visible = false;
@ -47,17 +51,21 @@ namespace Электронная_Фармация
AppServices.Initialize(_toastService);
_loadingOverlay = new LoadingOverlay { Name = "loadingOverlay", Dock = DockStyle.Fill };
_titleBar = new ModernTitleBar { Name = "titleBar" };
_titleBar.Attach(this);
_titleBar.ApplyTheme(ThemeManager.Colors);
_appHeader = new ModernAppHeader { Name = "appHeader", Dock = DockStyle.Top };
_appHeader.SetItems(new[]
{
new SidebarItem { Key = "price", Text = "ПРАЙС-ЛИСТ", IconGlyph = "\uE8FD" },
new SidebarItem { Key = "orders", Text = "ЗАКАЗЫ", IconGlyph = "\uE719" },
new SidebarItem { Key = "invoices", Text = "НАКЛАДНЫЕ", IconGlyph = "\uE8A5" },
new SidebarItem { Key = "upload", Text = "ЗАГРУЗИТЬ", IconGlyph = "\uE896" },
new SidebarItem { Key = "send", Text = "ОТПРАВИТЬ", IconGlyph = "\uE725" },
new SidebarItem { Key = "refusals", Text = "ОТКАЗЫ", IconGlyph = "\uE711" },
new SidebarItem { Key = "directory", Text = "СПРАВОЧНИК", IconGlyph = "\uE8FD" },
new SidebarItem { Key = "auth", Text = "АВТОРИЗАЦИЯ", IconGlyph = "\uE192" }
new SidebarItem { Key = "price", Text = райс-лист", IconGlyph = "\uE14C" },
new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" },
new SidebarItem { Key = "invoices", Text = "Накладные", IconGlyph = "\uE8A5" },
new SidebarItem { Key = "upload", Text = "Загрузить", IconGlyph = "\uE896" },
new SidebarItem { Key = "send", Text = "Отправить", IconGlyph = "\uE725" },
new SidebarItem { Key = "refusals", Text = "Отказы", IconGlyph = "\uE7BA" },
new SidebarItem { Key = "directory", Text = "Справочник", IconGlyph = "\uE8F1" },
new SidebarItem { Key = "auth", Text = "Вход", IconGlyph = "\uE192" }
});
_documentTabStrip = new ModernDocumentTabStrip { Name = "documentTabStrip", Dock = DockStyle.Top };
@ -67,16 +75,22 @@ namespace Электронная_Фармация
{
Name = "statusFooter",
Dock = DockStyle.Bottom,
Height = 28,
Height = 32,
Tag = "status-footer",
Padding = new Padding(10, 0, 4, 0)
Padding = new Padding(12, 0, 8, 0)
};
_statusFooter.Paint += (_, e) =>
{
var border = ThemeManager.Colors.Border;
using (var pen = new Pen(border))
var theme = ThemeManager.Colors;
var g = e.Graphics;
using (var fill = new SolidBrush(theme.CartHeaderBackground))
{
e.Graphics.DrawLine(pen, 0, 0, _statusFooter.Width, 0);
g.FillRectangle(fill, _statusFooter.ClientRectangle);
}
using (var pen = new Pen(theme.AccentPrimaryBorder))
{
g.DrawLine(pen, 0, 0, _statusFooter.Width, 0);
}
};
@ -118,6 +132,7 @@ namespace Электронная_Фармация
Controls.Add(_statusFooter);
Controls.Add(_documentTabStrip);
Controls.Add(_appHeader);
Controls.Add(_titleBar);
_directoryMenu = new ContextMenuStrip();
var contractorsItem = new ToolStripMenuItem("Поставщики");
@ -167,12 +182,24 @@ namespace Электронная_Фармация
{
UiThemeHelper.ApplyToControlTree(this);
UpdateThemeToggleState();
_titleBar?.ApplyTheme(ThemeManager.Colors);
WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
};
HandleCreated += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
}
protected override void WndProc(ref Message m)
{
if (FormBorderStyle == FormBorderStyle.None
&& WindowChromeHelper.TryHandleNcHitTest(this, ref m))
{
return;
}
base.WndProc(ref m);
}
private void UpdateThemeToggleState()
{
if (_documentTabStrip != null)
@ -193,14 +220,49 @@ namespace Электронная_Фармация
{
ThemeManager.CurrentVariant = Settings.Default.UseDarkTheme
? ThemeVariant.DarkProductivity
: ThemeVariant.PharmaTrust;
: ThemeVariant.ClinicalPro;
_themeService.Initialize(this);
UpdateThemeToggleState();
UiThemeHelper.ApplyToControlTree(this);
_titleBar?.ApplyTheme(ThemeManager.Colors);
WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
try
{
Icon = CreateAppIcon();
}
catch
{
// keep default
}
RefreshCurrentUserStatus();
}
private static Icon CreateAppIcon()
{
using (var bmp = new Bitmap(32, 32))
using (var g = Graphics.FromImage(bmp))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.Clear(Color.Transparent);
using (var brush = new SolidBrush(Color.FromArgb(15, 118, 110)))
{
g.FillEllipse(brush, 1, 1, 30, 30);
}
using (var pen = new Pen(Color.White, 3f)
{
StartCap = System.Drawing.Drawing2D.LineCap.Round,
EndCap = System.Drawing.Drawing2D.LineCap.Round
})
{
g.DrawLine(pen, 16, 8, 16, 24);
g.DrawLine(pen, 8, 16, 24, 16);
}
return Icon.FromHandle(bmp.GetHicon());
}
}
private void RefreshCurrentUserStatus()
{
if (_lblCurrentUser == null)

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@ -509,76 +509,17 @@ FROM [TempOrderItems];";
}
}
// Перед seed-вставкой грузополучателей нужно гарантировать наличие LocationId,
// иначе INSERT в Consignees (LocationId) упадёт на старых версиях БД.
// LocationId нужен для связи с BuyerLocation на сервере.
TryAddColumn(conForCheckTables, "Consignees", "LocationId", "nvarchar(64)");
// Seed-тестовые грузополучатели (аптеки) для удобства ручного тестирования.
// Вставляем только если таблица пустая.
using (var countConsigneesCmd = new SQLiteCommand(
@"SELECT COUNT(*) FROM Consignees;",
// Удаляем старые тестовые грузополучатели — точки создаются в веб-кабинете.
using (var delTest = new SQLiteCommand(
@"DELETE FROM Consignees
WHERE ConsigneesName LIKE 'Тестовая аптека%'
OR ConsigneesAddress LIKE 'Тестовый адрес%';",
conForCheckTables))
{
var countObj = countConsigneesCmd.ExecuteScalar();
var count = 0;
try { count = Convert.ToInt32(countObj); } catch { count = 0; }
if (count == 0)
{
var testLocationIds = new[]
{
"87368f6d-488b-41a9-b170-edc629522844",
"c4cd845f-820d-45ac-a473-3df1a0b96feb",
"d6187013-00f5-4c1b-b258-c670cf71d115"
};
// Тестовые имена/адреса — можно менять под вашу тестовую среду.
var testNames = new[]
{
"Тестовая аптека 1",
"Тестовая аптека 2",
"Тестовая аптека 3"
};
var testAddresses = new[]
{
"Тестовый адрес 1",
"Тестовый адрес 2",
"Тестовый адрес 3"
};
using (var tx = conForCheckTables.BeginTransaction())
{
int nextCode;
using (var nextCodeCmd = new SQLiteCommand(
@"SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees;",
conForCheckTables,
tx))
{
nextCode = Convert.ToInt32(nextCodeCmd.ExecuteScalar());
}
for (var i = 0; i < 3; i++)
{
using (var insertCmd = new SQLiteCommand(
@"INSERT INTO Consignees
(codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId)
VALUES (@code, @name, @address, @isDefault, @loc);",
conForCheckTables,
tx))
{
insertCmd.Parameters.AddWithValue("@code", nextCode + i);
insertCmd.Parameters.AddWithValue("@name", testNames[i]);
insertCmd.Parameters.AddWithValue("@address", testAddresses[i]);
insertCmd.Parameters.AddWithValue("@isDefault", i == 0 ? 1 : 0);
insertCmd.Parameters.AddWithValue("@loc", testLocationIds[i]);
insertCmd.ExecuteNonQuery();
}
}
tx.Commit();
}
}
delTest.ExecuteNonQuery();
}
TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)");
@ -600,7 +541,7 @@ FROM [TempOrderItems];";
{
try
{
var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
var logDir = AppConfig.LogsDirectory;
Directory.CreateDirectory(logDir);
var logPath = Path.Combine(
logDir,

View File

@ -1,8 +1,10 @@
using System;
using System.Data;
using System.Data.SQLite;
using System.Threading.Tasks;
using System.Windows.Forms;
using Электроннаяармация.Classes;
using Электроннаяармация.Properties;
namespace Электроннаяармация.Forms
{
@ -15,10 +17,12 @@ namespace Электронная_Фармация.Forms
InitializeComponent();
}
private void FConsignees_Load(object sender, EventArgs e)
private async void FConsignees_Load(object sender, EventArgs e)
{
UiThemeHelper.ApplyToControlTree(this);
btnAdd.Text = "С сервера";
showMeContentFromConsigneesTable();
await SyncFromServerAsync(silent: true);
}
void showMeContentFromConsigneesTable()
@ -49,7 +53,8 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
foreach (DataGridViewColumn column in dgvConsignees.Columns)
{
column.ReadOnly = column.Name != "Location ID" && column.Name != "Адрес";
// Редактируем только адрес локально; Location ID и имя приходят с сервера.
column.ReadOnly = column.Name != "Адрес";
}
}
catch (Exception ex)
@ -88,8 +93,6 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
dgvConsignees.EndEdit();
// Кто должен стать активной аптекой после сохранения.
// По вашему ТЗ: "аптека выбранная в этом окне" => текущая выделенная строка.
var selectedName = dgvConsignees.CurrentRow?.Cells["Наименование"]?.Value?.ToString()?.Trim()
?? string.Empty;
SelectedConsigneeNameAfterSave = selectedName;
@ -108,14 +111,12 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
using (var cmd = new SQLiteCommand(
@"UPDATE Consignees
SET ConsigneesAddress = @address,
LocationId = @locationId
SET ConsigneesAddress = @address
WHERE idConsignees = @id",
connection,
tx))
{
cmd.Parameters.AddWithValue("@address", row["Адрес"]?.ToString() ?? string.Empty);
cmd.Parameters.AddWithValue("@locationId", row["Location ID"]?.ToString()?.Trim() ?? string.Empty);
cmd.Parameters.AddWithValue("@id", Convert.ToInt64(row["Id"]));
cmd.ExecuteNonQuery();
}
@ -137,107 +138,58 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
Close();
}
private void btnAdd_Click(object sender, EventArgs e)
private async void btnAdd_Click(object sender, EventArgs e)
{
using (var dialog = new Form())
await SyncFromServerAsync(silent: false);
}
private async Task SyncFromServerAsync(bool silent)
{
try
{
dialog.Text = "Новая аптека";
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
dialog.StartPosition = FormStartPosition.CenterParent;
// Немного увеличиваем высоту/ширину на случай DPI/шрифтов.
dialog.ClientSize = new System.Drawing.Size(480, 310);
dialog.MaximizeBox = false;
dialog.MinimizeBox = false;
dialog.ShowInTaskbar = false;
var lblName = new Label { Text = "Наименование", Left = 16, Top = 16, AutoSize = true };
var txtName = new TextBox { Left = 16, Top = 42, Width = 440, Height = 28 };
var lblAddress = new Label { Text = "Адрес", Left = 16, Top = 82, AutoSize = true };
var txtAddress = new TextBox { Left = 16, Top = 108, Width = 440, Height = 28 };
var lblLocation = new Label { Text = "Location ID", Left = 16, Top = 148, AutoSize = true };
var txtLocation = new TextBox { Left = 16, Top = 174, Width = 440, Height = 28 };
var btnOk = new Elfisa.UI.Controls.ModernButton
btnAdd.Enabled = false;
var client = new ApiClient();
if (!client.IsAuthenticated)
{
Text = "Добавить",
DialogResult = DialogResult.OK
};
var btnCancel = new Elfisa.UI.Controls.ModernButton
{
Text = "Отмена",
DialogResult = DialogResult.Cancel
};
dialog.Controls.AddRange(new Control[]
{
lblName, txtName, lblAddress, txtAddress, lblLocation, txtLocation, btnOk, btnCancel
});
dialog.AcceptButton = btnOk;
dialog.CancelButton = btnCancel;
UiThemeHelper.ApplyToControlTree(dialog);
btnOk.Padding = new Padding(12, 4, 12, 4);
btnCancel.Padding = new Padding(12, 4, 12, 4);
Elfisa.UI.Controls.ModernButtonStyles.FitToText(btnOk, 36);
Elfisa.UI.Controls.ModernButtonStyles.FitToText(btnCancel, 36);
btnOk.Height = 36;
btnCancel.Height = 36;
const int padding = 16;
const int gap = 10;
// Минимально гарантируем ширину, чтобы обе кнопки поместились.
var requiredWidth = btnOk.Width + btnCancel.Width + gap + padding * 2;
if (dialog.ClientSize.Width < requiredWidth)
{
dialog.ClientSize = new System.Drawing.Size(requiredWidth, dialog.ClientSize.Height);
}
// Всегда зажимаем в границы формы после вычисления ширины ModernButton'ов.
btnCancel.Left = Math.Max(padding, dialog.ClientSize.Width - btnCancel.Width - padding);
btnCancel.Top = dialog.ClientSize.Height - btnCancel.Height - padding;
btnOk.Left = Math.Max(padding, btnCancel.Left - btnOk.Width - gap);
btnOk.Top = btnCancel.Top;
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
var name = (txtName.Text ?? string.Empty).Trim();
var address = (txtAddress.Text ?? string.Empty).Trim();
var locationId = (txtLocation.Text ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(name))
{
UiDialogs.ShowError("Укажите наименование аптеки.", "Грузополучатели", this);
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
int nextCode = 1;
using (var codeCmd = new SQLiteCommand("SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees", connection))
var token = Settings.Default.stringToken;
if (string.IsNullOrWhiteSpace(token))
{
nextCode = Convert.ToInt32(codeCmd.ExecuteScalar());
}
using (var insert = new SQLiteCommand(
@"INSERT INTO Consignees (codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId)
VALUES (@code, @name, @address, 0, @locationId)",
connection))
{
insert.Parameters.AddWithValue("@code", nextCode);
insert.Parameters.AddWithValue("@name", name);
insert.Parameters.AddWithValue("@address", address);
insert.Parameters.AddWithValue("@locationId", locationId);
insert.ExecuteNonQuery();
if (!silent)
{
UiDialogs.ShowInfo(
"Сначала войдите на сервер — грузополучатели подтягиваются из веб-кабинета покупателя.",
"Грузополучатели",
this);
}
return;
}
client.SetToken(token);
}
ToastNotification.ShowSuccess("Аптека добавлена");
var locations = await client.GetBuyerLocationsAsync();
var count = ConsigneeHelper.SyncFromServer(locations);
showMeContentFromConsigneesTable();
if (!silent)
{
ToastNotification.ShowSuccess(count > 0
? $"Обновлено грузополучателей: {count}"
: "На сервере пока нет грузополучателей");
}
}
catch (Exception ex)
{
if (!silent)
{
UiDialogs.ShowError($"Не удалось загрузить грузополучателей с сервера.\n{ex.Message}", "Грузополучатели", this);
}
else
{
AppDebugLog.Error("FConsignees", "Тихая синхронизация грузополучателей не удалась", ex);
}
}
finally
{
btnAdd.Enabled = true;
}
}
@ -249,15 +201,15 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
return;
}
var name = dgvConsignees.CurrentRow.Cells["Наименование"]?.Value?.ToString();
var name = dgvConsignees.CurrentRow.Cells["Наименование"]?.Value?.ToString()?.Trim();
if (string.IsNullOrWhiteSpace(name))
{
return;
}
ConsigneeHelper.SetActiveConsignee(name);
ToastNotification.ShowSuccess($"Аптека по умолчанию: {name}");
showMeContentFromConsigneesTable();
ToastNotification.ShowSuccess($"Аптека по умолчанию: {name}");
}
}
}

View File

@ -54,6 +54,21 @@ namespace Электронная_Фармация.HelpForms
SetOverallProgress(1, "Авторизация...");
await EnsureAuthenticatedAsync(client);
try
{
AppendStatus("Загружаю грузополучателей с сервера...");
var locations = await client.GetBuyerLocationsAsync();
var synced = ConsigneeHelper.SyncFromServer(locations);
AppendStatus(synced > 0
? $"Грузополучателей обновлено: {synced}"
: "Грузополучателей на сервере пока нет");
}
catch (Exception locEx)
{
AppendStatus($"Грузополучатели не обновлены: {locEx.Message}");
AppDebugLog.Error("Download", "Не удалось синхронизировать грузополучателей", locEx);
}
SetOverallProgress(3, "Загрузка прайса с сервера...");
AppendStatus("Загружаю сводный прайс...");
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);

View File

@ -1,36 +1,37 @@
using System.Reflection;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.
[assembly: AssemblyTitle("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>")]
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ. пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ,
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ.
[assembly: AssemblyTitle("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>")]
[assembly: AssemblyCopyright("Copyright <EFBFBD> 2023")]
[assembly: AssemblyProduct("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")]
[assembly: AssemblyCopyright("Copyright пїЅ 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> False <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ComVisible <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> COM. <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
// COM, <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ComVisible <20> TRUE <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>.
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ False пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ COM. пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ
// COM, пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅ TRUE пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ.
[assembly: ComVisible(false)]
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> GUID <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> COM
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ GUID пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ, пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ COM
[assembly: Guid("145d8060-4187-4871-b4fb-8df795727b66")]
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ:
//
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
//
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> "*", <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>:
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.20.0")]
[assembly: AssemblyFileVersion("1.0.20.0")]
[assembly: AssemblyVersion("1.0.21.0")]
[assembly: AssemblyFileVersion("1.0.21.0")]

View File

@ -131,6 +131,18 @@ namespace Электронная_Фармация.Properties {
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
public int PriceListSplitterDistance {
get {
return ((int)(this["PriceListSplitterDistance"]));
}
set {
this["PriceListSplitterDistance"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]

View File

@ -29,6 +29,9 @@
<Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="PriceListSplitterDistance" Type="System.Int32" Scope="User">
<Value Profile="(Default)">0</Value>
</Setting>
<Setting Name="InvoiceExportPath" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -1,4 +1,4 @@
namespace Электроннаяармация.UserControls
namespace Электроннаяармация.UserControls
{
partial class UCPriceList
{
@ -99,7 +99,7 @@
this.SplitPriceList.Panel2.Controls.Add(this.POrderInfo);
this.SplitPriceList.Size = new System.Drawing.Size(1053, 532);
this.SplitPriceList.SplitterDistance = 290;
this.SplitPriceList.SplitterWidth = 2;
this.SplitPriceList.SplitterWidth = 6;
this.SplitPriceList.TabIndex = 0;
//
// TLPPriceList

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@ -78,13 +78,14 @@ namespace Электронная_Фармация.UserControls
private void UCPriceList_Load(object sender, EventArgs e)
{
EnsureBrowseSplit();
EnsureChromeLayout();
UiThemeHelper.ApplyToControlTree(this);
Tag = "chrome-child";
AlignSearchRow();
LayoutContentBelowSearch();
panel1.SizeChanged += (_, __) => LayoutMarkupRow();
LayoutMarkupRow();
SizeChanged += (_, __) => LayoutContentBelowSearch();
txtConsignee.Enabled = false;
txtConsignee.Text = _WorkWithConsignee;
@ -97,6 +98,120 @@ namespace Электронная_Фармация.UserControls
txtSearchGoodNameInPriceList.Focus();
}
/// <summary>
/// Поиск остаётся сверху как в оригинале; контент явно ниже — без Dock-наложения.
/// </summary>
private void EnsureChromeLayout()
{
// Убираем старую dock-панель, если осталась от прошлых сборок в той же сессии.
var oldToolbar = Controls["priceToolbar"];
if (oldToolbar != null)
{
var toRestore = oldToolbar.Controls.Cast<Control>().ToArray();
foreach (var control in toRestore)
{
oldToolbar.Controls.Remove(control);
Controls.Add(control);
control.BringToFront();
}
Controls.Remove(oldToolbar);
oldToolbar.Dispose();
}
// Строка наценки скрыта — проценты берутся из настроек/дефолта без UI-прослойки.
if (TLPPriceList.RowStyles.Count >= 2)
{
TLPPriceList.RowStyles[1].SizeType = SizeType.Absolute;
TLPPriceList.RowStyles[1].Height = 0;
}
panel1.Visible = false;
panel1.Height = 0;
SplitPriceList.Panel1MinSize = 120;
SplitPriceList.Panel2MinSize = 80;
SplitPriceList.IsSplitterFixed = false;
SplitPriceList.FixedPanel = FixedPanel.None;
SplitPriceList.SplitterWidth = 6;
// Восстановить положение сплиттера корзины, если пользователь уже двигал.
RestoreCartSplitterDistance();
SplitPriceList.SplitterMoved += (_, __) => PersistCartSplitterDistance();
dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None;
dgvPriceList.ColumnHeadersVisible = true;
dgvPriceList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
if (dgvPriceList.ColumnHeadersHeight < 34)
{
dgvPriceList.ColumnHeadersHeight = 36;
}
if (!POrderInfo.Controls.ContainsKey("cartHeader"))
{
var cartHeader = new ModernSectionHeader
{
Name = "cartHeader",
Height = 32,
HeaderText = "Корзина"
};
cartHeader.ApplyTheme(Elfisa.UI.Theming.ThemeManager.Colors, cartStyle: true);
cartHeader.Dock = DockStyle.Top;
POrderInfo.Controls.Add(cartHeader);
tabOrderItems.SendToBack();
cartHeader.BringToFront();
}
}
private void LayoutContentBelowSearch()
{
const int searchBand = 52;
PContentPriceList.Dock = DockStyle.None;
PContentPriceList.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
PContentPriceList.SetBounds(
0,
searchBand,
Math.Max(100, ClientSize.Width),
Math.Max(120, ClientSize.Height - searchBand));
}
private void RestoreCartSplitterDistance()
{
try
{
var saved = Properties.Settings.Default.PriceListSplitterDistance;
if (saved <= 0 || SplitPriceList.Height < 200)
{
return;
}
var max = Math.Max(
SplitPriceList.Panel1MinSize,
SplitPriceList.Height - SplitPriceList.Panel2MinSize - SplitPriceList.SplitterWidth);
SplitPriceList.SplitterDistance = Math.Max(SplitPriceList.Panel1MinSize, Math.Min(saved, max));
}
catch
{
// ignore until layout ready
}
}
private void PersistCartSplitterDistance()
{
try
{
if (SplitPriceList.SplitterDistance > 80)
{
Properties.Settings.Default.PriceListSplitterDistance = SplitPriceList.SplitterDistance;
Properties.Settings.Default.Save();
}
}
catch
{
// ignore
}
}
private void EnsureBrowseSplit()
{
if (splitNameMgBrowse != null)
@ -112,13 +227,35 @@ namespace Электронная_Фармация.UserControls
Name = "splitNameMgBrowse",
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical,
SplitterWidth = 4,
Visible = false
SplitterWidth = 6,
Visible = false,
Panel1MinSize = 100,
Panel2MinSize = 80,
IsSplitterFixed = false,
FixedPanel = FixedPanel.None
};
splitNameMgBrowse.Panel1.Controls.Add(dgvBrowseNames);
splitNameMgBrowse.Panel2.Controls.Add(dgvBrowseMg);
TLPPriceList.Controls.Add(splitNameMgBrowse, 0, 0);
var gridHost = TLPPriceList.Controls["priceGridHost"] as Panel;
if (gridHost == null)
{
gridHost = new Panel
{
Name = "priceGridHost",
Dock = DockStyle.Fill,
Margin = new Padding(0),
Padding = new Padding(0),
Tag = "chrome-child"
};
TLPPriceList.Controls.Remove(dgvPriceList);
dgvPriceList.Dock = DockStyle.Fill;
gridHost.Controls.Add(dgvPriceList);
TLPPriceList.Controls.Add(gridHost, 0, 0);
}
gridHost.Controls.Add(splitNameMgBrowse);
splitNameMgBrowse.BringToFront();
dgvBrowseNames.SelectionChanged += dgvBrowseNames_SelectionChanged;
@ -157,11 +294,13 @@ namespace Электронная_Фармация.UserControls
private void AlignSearchRow()
{
const int searchY = 9;
const int searchY = 10;
const int searchHeight = 32;
const int clearGap = 2;
const int clearGap = 4;
const int leftPad = 10;
txtSearchGoodNameInPriceList.SetBounds(78, searchY, 313, searchHeight);
label2.Location = new Point(leftPad, searchY + 4);
txtSearchGoodNameInPriceList.SetBounds(label2.Right + 6, searchY, 300, searchHeight);
btnClearSearchInPriceList.SetBounds(
txtSearchGoodNameInPriceList.Right + clearGap,
searchY,
@ -169,38 +308,23 @@ namespace Электронная_Фармация.UserControls
searchHeight);
ModernButtonStyles.ApplyClear(btnClearSearchInPriceList);
lblSupplierFilter.Location = new Point(btnClearSearchInPriceList.Right + 16, searchY + 4);
comboSupplierFilter.SetBounds(lblSupplierFilter.Right + 6, searchY, 240, searchHeight);
btnClearSupplierFilter.SetBounds(comboSupplierFilter.Right + clearGap, searchY, 32, searchHeight);
btnClearSupplierFilter.Height = searchHeight;
// Поиск всегда поверх контента, но контент начинается ниже searchBand.
label2.BringToFront();
txtSearchGoodNameInPriceList.BringToFront();
btnClearSearchInPriceList.BringToFront();
lblSupplierFilter.BringToFront();
comboSupplierFilter.BringToFront();
btnClearSupplierFilter.BringToFront();
}
private void LayoutMarkupRow()
{
// Keep markup row readable at different DPI/window sizes.
try
{
var paddingLeft = 8;
var y = 30;
var gap = 8;
label4.Location = new Point(paddingLeft, y + 2);
numericPercent.Location = new Point(label4.Right + 6, y);
numericPercent.Width = 56;
checkUsePercentageAsDefault.AutoSize = true;
checkUsePercentageAsDefault.Location = new Point(
Math.Max(numericPercent.Right + gap, panel1.ClientSize.Width - checkUsePercentageAsDefault.PreferredSize.Width - 10),
y + 2);
lblPriceForGood.AutoSize = false;
lblPriceForGood.AutoEllipsis = true;
lblPriceForGood.Location = new Point(numericPercent.Right + gap, y + 2);
lblPriceForGood.Height = 22;
lblPriceForGood.Width = Math.Max(90, checkUsePercentageAsDefault.Left - lblPriceForGood.Left - gap);
}
catch
{
// best-effort layout; ignore
}
// UI наценки скрыт; метод оставлен на случай повторного включения.
}
private void InitializeSupplierFilter()
@ -725,10 +849,21 @@ namespace Электронная_Фармация.UserControls
{
EnsureBrowseSplit();
var gridHost = TLPPriceList.Controls["priceGridHost"] as Panel;
if (gridHost != null)
{
gridHost.Dock = DockStyle.Fill;
if (gridHost.Padding.Top < 8)
{
gridHost.Padding = new Padding(0, 8, 0, 0);
}
}
if (browseVisible)
{
dgvPriceList.Visible = false;
splitNameMgBrowse.Visible = true;
splitNameMgBrowse.Dock = DockStyle.Fill;
splitNameMgBrowse.BringToFront();
var width = Math.Max(200, splitNameMgBrowse.Width);
@ -754,6 +889,9 @@ namespace Электронная_Фармация.UserControls
splitNameMgBrowse.Visible = false;
dgvPriceList.Visible = true;
dgvPriceList.Dock = DockStyle.Fill;
dgvPriceList.ColumnHeadersVisible = true;
dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None;
dgvPriceList.BringToFront();
}
}

View File

@ -9,7 +9,7 @@ using Elfisa.UI.Theming;
namespace Elfisa.UI.Controls
{
/// <summary>
/// Верхняя синяя шапка как в оригинале: 3 равные кнопки, owner-draw + плавный hover (Fluent).
/// Верхняя навигация: бренд + пункты (иконка сверху, подпись снизу).
/// </summary>
public class ModernAppHeader : Panel
{
@ -21,11 +21,13 @@ namespace Elfisa.UI.Controls
private string _hoverKey;
private float _hoverProgress;
private const int BrandWidth = 118;
public event EventHandler<string> ItemSelected;
public ModernAppHeader()
{
Height = 78;
Height = 64;
Dock = DockStyle.Top;
Cursor = Cursors.Hand;
SetStyle(ControlStyles.AllPaintingInWmPaint |
@ -64,21 +66,16 @@ namespace Elfisa.UI.Controls
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
var rect = ClientRectangle;
using (var brush = new LinearGradientBrush(rect, theme.TitleBarTop, theme.TitleBarBottom, 90f))
// Сплошной основной teal — без градиента, в тон AccentPrimary.
using (var fill = new SolidBrush(theme.AccentPrimary))
{
g.FillRectangle(brush, rect);
g.FillRectangle(fill, rect);
}
using (var sheen = new LinearGradientBrush(
new Rectangle(0, 0, Width, Height / 2),
Color.FromArgb(28, 255, 255, 255),
Color.FromArgb(0, 255, 255, 255),
LinearGradientMode.Vertical))
{
g.FillRectangle(sheen, 0, 0, Width, Height / 2);
}
DrawBrand(g, theme);
if (_items.Count == 0)
{
@ -86,72 +83,125 @@ namespace Elfisa.UI.Controls
}
_hitAreas.Clear();
var gap = 8;
var btnWidth = (Width - gap * (_items.Count + 1)) / _items.Count;
var btnHeight = Height - 12;
var top = 6;
const int gap = 6;
const int padRight = 8;
var btnHeight = Height - 10;
var top = 5;
var x = BrandWidth + 6;
using (var iconFont = new Font("Segoe MDL2 Assets", 18f))
using (var textFont = new Font("Segoe UI", 10f, FontStyle.Bold))
using (var iconFont = FluentFonts.CreateIconFont(18f))
using (var textFont = new Font("Segoe UI Semibold", 8.5f, FontStyle.Bold))
{
var iconFormat = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
FormatFlags = StringFormatFlags.NoWrap
};
// Равная ширина слотов — подписи целиком помещаются на широком окне.
var available = Math.Max(100, Width - BrandWidth - padRight - 6);
var btnWidth = Math.Max(72, (available - gap * (_items.Count - 1)) / _items.Count);
for (var i = 0; i < _items.Count; i++)
{
var item = _items[i];
var btnRect = new Rectangle(gap + i * (btnWidth + gap), top, btnWidth, btnHeight);
var btnRect = new Rectangle(x, top, btnWidth, btnHeight);
_hitAreas[item.Key] = btnRect;
x += btnWidth + gap;
var isActive = item.Key == _activeKey;
var isHover = item.Key == _hoverKey;
var hoverBoost = isHover ? _hoverProgress * 0.22f : 0f;
var baseAlpha = isActive ? 0.28f : 0.08f;
var alpha = (int)(255 * Math.Min(0.55f, baseAlpha + hoverBoost));
var fill = Color.FromArgb(alpha, 255, 255, 255);
var hoverBoost = isHover ? _hoverProgress * 0.18f : 0f;
var baseAlpha = isActive ? 0.34f : 0.07f;
var alpha = (int)(255 * Math.Min(0.52f, baseAlpha + hoverBoost));
using (var fillBrush = new SolidBrush(fill))
using (var fillBrush = new SolidBrush(Color.FromArgb(alpha, 255, 255, 255)))
{
GraphicsExtensions.FillRoundedRectangle(g, fillBrush, btnRect, theme.CornerRadiusSmall);
}
if (isActive)
{
using (var pen = new Pen(Color.FromArgb(180, 255, 255, 255), 1f))
using (var pen = new Pen(Color.FromArgb(210, 255, 255, 255), 1.1f))
{
GraphicsExtensions.DrawRoundedRectangle(g, pen, btnRect, theme.CornerRadiusSmall);
}
using (var accent = new SolidBrush(Color.FromArgb(235, 255, 255, 255)))
{
g.FillRectangle(accent, btnRect.X + 10, btnRect.Bottom - 3, btnRect.Width - 20, 2);
}
}
else if (isHover && _hoverProgress > 0.05f)
{
using (var pen = new Pen(Color.FromArgb((int)(120 * _hoverProgress), 255, 255, 255), 1f))
using (var pen = new Pen(Color.FromArgb((int)(110 * _hoverProgress), 255, 255, 255), 1f))
{
GraphicsExtensions.DrawRoundedRectangle(g, pen, btnRect, theme.CornerRadiusSmall);
}
}
var iconRect = new Rectangle(btnRect.X, btnRect.Y + 8, btnRect.Width, 26);
var textRect = new Rectangle(btnRect.X, btnRect.Y + 34, btnRect.Width, btnRect.Height - 34);
if (!string.IsNullOrEmpty(item.IconGlyph))
{
TextRenderer.DrawText(
g,
item.IconGlyph,
iconFont,
iconRect,
Color.White,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine);
var iconRect = new RectangleF(btnRect.X, btnRect.Y + 4, btnRect.Width, 26);
using (var brush = new SolidBrush(Color.White))
{
g.DrawString(item.IconGlyph, iconFont, brush, iconRect, iconFormat);
}
}
var textRect = new Rectangle(btnRect.X + 2, btnRect.Y + 30, btnRect.Width - 4, btnRect.Height - 34);
TextRenderer.DrawText(
g,
item.Text,
textFont,
textRect,
Color.White,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine);
TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine);
}
}
}
private void DrawBrand(Graphics g, ThemeColors theme)
{
var brandRect = new Rectangle(0, 0, BrandWidth, Height);
using (var dim = new SolidBrush(Color.FromArgb(28, 0, 0, 0)))
{
g.FillRectangle(dim, brandRect);
}
using (var pen = new Pen(Color.FromArgb(50, 255, 255, 255), 1f))
{
g.DrawLine(pen, BrandWidth - 1, 8, BrandWidth - 1, Height - 8);
}
var iconSize = 22;
var iconRect = new Rectangle(12, (Height - iconSize) / 2, iconSize, iconSize);
using (var iconBrush = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
{
GraphicsExtensions.FillRoundedRectangle(g, iconBrush, iconRect, 6);
}
using (var crossPen = new Pen(Color.White, 2f) { StartCap = LineCap.Round, EndCap = LineCap.Round })
{
var cx = iconRect.X + iconRect.Width / 2f;
var cy = iconRect.Y + iconRect.Height / 2f;
g.DrawLine(crossPen, cx - 5, cy, cx + 5, cy);
g.DrawLine(crossPen, cx, cy - 5, cx, cy + 5);
}
using (var brandFont = new Font("Segoe UI Semibold", 11f, FontStyle.Bold))
{
TextRenderer.DrawText(
g,
"ЭльФиСА",
brandFont,
new Rectangle(40, 0, BrandWidth - 46, Height),
Color.White,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.SingleLine);
}
}
protected override void OnPaintBackground(PaintEventArgs e)
{
}

View File

@ -445,13 +445,15 @@ namespace Elfisa.UI.Controls
else
{
var borderColor = _buttonStyle == ModernButtonStyle.DangerSoft
? GraphicsExtensions.Blend(theme.Danger, Color.White, 0.55f)
? GraphicsExtensions.Blend(theme.Danger, Color.White, 0.45f)
: _buttonStyle == ModernButtonStyle.Subtle
? theme.Border
: theme.InputBorder;
if (_hover && Enabled)
{
borderColor = theme.InputBorderFocus;
borderColor = _buttonStyle == ModernButtonStyle.DangerSoft
? theme.Danger
: theme.InputBorderFocus;
}
using (var pen = new Pen(borderColor, 1f))

View File

@ -30,7 +30,8 @@ namespace Elfisa.UI.Controls
if (IsDangerAction(text))
{
ApplyAction(button, ModernButtonStyle.Danger);
// Мягкий danger в тон Clinical Pro — без кислотно-красного «крика».
ApplyAction(button, ModernButtonStyle.DangerSoft);
return;
}
@ -73,7 +74,7 @@ namespace Elfisa.UI.Controls
return;
}
button.ButtonStyle = ModernButtonStyle.Danger;
button.ButtonStyle = ModernButtonStyle.DangerSoft;
button.CompactIconMode = true;
button.Image = null;
button.BackgroundImage = null;

View File

@ -177,11 +177,18 @@ namespace Elfisa.UI.Controls
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
using (var bgBrush = new SolidBrush(theme.BackgroundSecondary))
// Та же «кожа», что у шапки/футера: лёгкий teal-тинт, не голый slate.
var stripBg = GraphicsExtensions.Blend(theme.Surface, theme.AccentPrimarySoft, 0.55f);
using (var bgBrush = new SolidBrush(stripBg))
{
g.FillRectangle(bgBrush, ClientRectangle);
}
using (var edge = new Pen(theme.AccentPrimaryBorder, 1f))
{
g.DrawLine(edge, 0, Height - 1, Width, Height - 1);
}
if (_tabs.Count == 0)
{
return;
@ -198,8 +205,8 @@ namespace Elfisa.UI.Controls
const int toggleReserve = 44;
var maxTabRight = Math.Max(10, Width - toggleReserve);
using (var closeFont = FluentFonts.CreateIconFont(10f))
using (var textFont = theme.FontSemibold14 ?? new Font("Segoe UI", 10f, FontStyle.Bold))
using (var closeFont = new Font("Segoe MDL2 Assets", 10f, FontStyle.Regular))
{
foreach (var tab in _tabs)
{
@ -218,25 +225,33 @@ namespace Elfisa.UI.Controls
var backColor = isActive
? theme.SurfaceElevated
: theme.Surface;
: GraphicsExtensions.Blend(theme.Surface, theme.AccentPrimarySoft, 0.35f);
if (!isActive && isHover)
{
backColor = GraphicsExtensions.Blend(backColor, theme.AccentPrimarySoft, 0.35f);
backColor = GraphicsExtensions.Blend(backColor, theme.AccentPrimarySoft, 0.65f);
}
var foreColor = isActive ? theme.TextPrimary : theme.TextSecondary;
var borderColor = isActive ? theme.AccentPrimaryBorder : theme.BorderSubtle;
var foreColor = isActive ? theme.AccentPrimary : theme.TextMuted;
var borderColor = isActive ? theme.AccentPrimary : theme.AccentPrimaryBorder;
using (var brush = new SolidBrush(backColor))
{
GraphicsExtensions.FillRoundedRectangle(g, brush, tabRect, theme.CornerRadiusSmall);
}
using (var pen = new Pen(borderColor, isActive ? 1.2f : 1f))
using (var pen = new Pen(borderColor, isActive ? 1.4f : 1f))
{
GraphicsExtensions.DrawRoundedRectangle(g, pen, tabRect, theme.CornerRadiusSmall);
}
if (isActive)
{
using (var accent = new SolidBrush(theme.AccentPrimary))
{
g.FillRectangle(accent, tabRect.X + 10, tabRect.Bottom - 3, tabRect.Width - 20, 2);
}
}
var closeRect = new Rectangle(tabRect.Right - closeSize - 8, tabRect.Y + (tabRect.Height - closeSize) / 2, closeSize, closeSize);
_closeHitAreas[tab.Id] = closeRect;
@ -291,7 +306,7 @@ namespace Elfisa.UI.Controls
}
var glyph = _isDarkTheme ? "\uE706" : "\uE708";
using (var iconFont = new Font("Segoe MDL2 Assets", 11f))
using (var iconFont = FluentFonts.CreateIconFont(12f))
{
TextRenderer.DrawText(
g,

View File

@ -10,7 +10,9 @@ namespace Elfisa.UI.Controls
{
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
BorderStyle = BorderStyle.None;
SplitterWidth = 2;
SplitterWidth = 6;
IsSplitterFixed = false;
FixedPanel = FixedPanel.None;
}
public void ApplyTheme(ThemeColors theme)

View File

@ -0,0 +1,346 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Elfisa.UI.Theming;
namespace Elfisa.UI.Controls
{
/// <summary>
/// Системная шапка: заголовок + свернуть/развернуть/закрыть.
/// Иконки рисуются геометрией (не глифы) — как в Win11, без «ломаных» кнопок.
/// </summary>
public sealed class ModernTitleBar : Panel
{
private const int ButtonWidth = 40;
private enum ChromeKind { None, Minimize, Maximize, Close }
private Form _owner;
private ThemeColors _theme;
private ChromeKind _hover = ChromeKind.None;
private ChromeKind _pressed = ChromeKind.None;
private bool _isMaximized;
private readonly ToolTip _tip = new ToolTip { ShowAlways = false, AutoPopDelay = 2000 };
public ModernTitleBar()
{
Height = 30;
Dock = DockStyle.Top;
Cursor = Cursors.Default;
DoubleBuffered = true;
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.ResizeRedraw |
ControlStyles.Selectable, true);
TabStop = false;
ApplyTheme(ThemeManager.Colors);
}
public void Attach(Form owner)
{
_owner = owner;
if (_owner == null)
{
return;
}
_owner.TextChanged += (_, __) => Invalidate();
_owner.Resize += (_, __) =>
{
_isMaximized = _owner.WindowState == FormWindowState.Maximized;
Invalidate();
};
_isMaximized = _owner.WindowState == FormWindowState.Maximized;
Invalidate();
}
public void ApplyTheme(ThemeColors theme)
{
_theme = theme ?? ThemeManager.Colors;
BackColor = _theme.AccentPrimary;
Invalidate();
}
protected override void OnPaintBackground(PaintEventArgs e)
{
}
protected override void OnPaint(PaintEventArgs e)
{
var theme = _theme ?? ThemeManager.Colors;
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
using (var fill = new SolidBrush(theme.AccentPrimary))
{
g.FillRectangle(fill, ClientRectangle);
}
DrawTitle(g, theme);
DrawChromeButton(g, ChromeKind.Minimize, theme);
DrawChromeButton(g, ChromeKind.Maximize, theme);
DrawChromeButton(g, ChromeKind.Close, theme);
}
private void DrawTitle(Graphics g, ThemeColors theme)
{
var title = _owner == null || string.IsNullOrWhiteSpace(_owner.Text)
? "Электронная Фармация"
: _owner.Text;
var textRect = new Rectangle(12, 0, Math.Max(40, Width - ButtonWidth * 3 - 16), Height);
using (var font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold))
{
TextRenderer.DrawText(
g,
title,
font,
textRect,
theme.TextOnAccent,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine);
}
}
private Rectangle GetButtonRect(ChromeKind kind)
{
var index = kind == ChromeKind.Close ? 0 : kind == ChromeKind.Maximize ? 1 : 2;
return new Rectangle(Width - ButtonWidth * (index + 1), 0, ButtonWidth, Height);
}
private void DrawChromeButton(Graphics g, ChromeKind kind, ThemeColors theme)
{
var rect = GetButtonRect(kind);
var isHover = _hover == kind;
var isPressed = _pressed == kind;
if (isHover || isPressed)
{
Color hoverFill;
if (kind == ChromeKind.Close)
{
hoverFill = isPressed
? Color.FromArgb(180, 32, 28)
: Color.FromArgb(232, 17, 35);
}
else
{
var alpha = isPressed ? 55 : 38;
hoverFill = Color.FromArgb(alpha, 255, 255, 255);
}
using (var brush = new SolidBrush(hoverFill))
{
g.FillRectangle(brush, rect);
}
}
var iconColor = theme.TextOnAccent;
var cx = rect.X + rect.Width / 2f;
var cy = rect.Y + rect.Height / 2f;
using (var pen = new Pen(iconColor, 1.15f))
{
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
pen.LineJoin = LineJoin.Round;
switch (kind)
{
case ChromeKind.Minimize:
g.DrawLine(pen, cx - 5f, cy, cx + 5f, cy);
break;
case ChromeKind.Maximize:
if (_isMaximized)
{
// restore: два смещённых квадрата
var back = new RectangleF(cx - 3.2f, cy - 5.2f, 8.5f, 8.5f);
var front = new RectangleF(cx - 5.2f, cy - 3.2f, 8.5f, 8.5f);
g.DrawRectangle(pen, back.X, back.Y, back.Width, back.Height);
// залить передний квадрат цветом шапки (с учётом hover), чтобы «обрезать» задний
var coverColor = theme.AccentPrimary;
if (isHover || isPressed)
{
coverColor = Color.FromArgb(
255,
BlendChannel(theme.AccentPrimary.R, 255, isPressed ? 0.22f : 0.15f),
BlendChannel(theme.AccentPrimary.G, 255, isPressed ? 0.22f : 0.15f),
BlendChannel(theme.AccentPrimary.B, 255, isPressed ? 0.22f : 0.15f));
}
using (var cover = new SolidBrush(coverColor))
{
g.FillRectangle(cover, Rectangle.Round(RectangleF.Inflate(front, 1f, 1f)));
}
g.DrawRectangle(pen, front.X, front.Y, front.Width, front.Height);
}
else
{
var box = new RectangleF(cx - 4.5f, cy - 4.5f, 9f, 9f);
g.DrawRectangle(pen, box.X, box.Y, box.Width, box.Height);
}
break;
case ChromeKind.Close:
g.DrawLine(pen, cx - 4.5f, cy - 4.5f, cx + 4.5f, cy + 4.5f);
g.DrawLine(pen, cx + 4.5f, cy - 4.5f, cx - 4.5f, cy + 4.5f);
break;
}
}
}
private ChromeKind HitTest(Point p)
{
if (GetButtonRect(ChromeKind.Close).Contains(p)) return ChromeKind.Close;
if (GetButtonRect(ChromeKind.Maximize).Contains(p)) return ChromeKind.Maximize;
if (GetButtonRect(ChromeKind.Minimize).Contains(p)) return ChromeKind.Minimize;
return ChromeKind.None;
}
protected override void OnMouseMove(MouseEventArgs e)
{
var hit = HitTest(e.Location);
if (hit != _hover)
{
_hover = hit;
Cursor = hit == ChromeKind.None ? Cursors.Default : Cursors.Hand;
UpdateTip(hit);
Invalidate();
}
base.OnMouseMove(e);
}
protected override void OnMouseLeave(EventArgs e)
{
if (_hover != ChromeKind.None || _pressed != ChromeKind.None)
{
_hover = ChromeKind.None;
_pressed = ChromeKind.None;
Cursor = Cursors.Default;
_tip.Hide(this);
Invalidate();
}
base.OnMouseLeave(e);
}
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
{
base.OnMouseDown(e);
return;
}
var hit = HitTest(e.Location);
if (hit != ChromeKind.None)
{
_pressed = hit;
Invalidate();
base.OnMouseDown(e);
return;
}
if (e.Clicks >= 2)
{
ToggleMaximize();
}
else if (_owner != null)
{
ReleaseCapture();
SendMessage(_owner.Handle, 0xA1, (IntPtr)2, IntPtr.Zero);
}
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && _pressed != ChromeKind.None)
{
var hit = HitTest(e.Location);
var action = _pressed;
_pressed = ChromeKind.None;
Invalidate();
if (hit == action)
{
Execute(action);
}
}
base.OnMouseUp(e);
}
private void Execute(ChromeKind kind)
{
if (_owner == null)
{
return;
}
switch (kind)
{
case ChromeKind.Minimize:
_owner.WindowState = FormWindowState.Minimized;
break;
case ChromeKind.Maximize:
ToggleMaximize();
break;
case ChromeKind.Close:
_owner.Close();
break;
}
}
private void ToggleMaximize()
{
if (_owner == null)
{
return;
}
_owner.WindowState = _owner.WindowState == FormWindowState.Maximized
? FormWindowState.Normal
: FormWindowState.Maximized;
_isMaximized = _owner.WindowState == FormWindowState.Maximized;
Invalidate();
}
private void UpdateTip(ChromeKind kind)
{
string text = null;
switch (kind)
{
case ChromeKind.Minimize: text = "Свернуть"; break;
case ChromeKind.Maximize: text = _isMaximized ? "Свернуть в окно" : "Развернуть"; break;
case ChromeKind.Close: text = "Закрыть"; break;
}
if (text == null)
{
_tip.Hide(this);
return;
}
var rect = GetButtonRect(kind);
_tip.Show(text, this, rect.X + rect.Width / 2, Height + 2, 1500);
}
private static int BlendChannel(int from, int to, float amount)
{
return (int)(from + (to - from) * amount);
}
[DllImport("user32.dll")]
private static extern bool ReleaseCapture();
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
}
}

View File

@ -8,15 +8,15 @@ using Elfisa.UI.Theming;
namespace Elfisa.UI.Controls
{
/// <summary>
/// Экран «Загрузить» — карточка с пунктирной рамкой и кнопками (Pharma Trust).
/// Экран «Загрузить» — компактная карточка по центру с действиями синхронизации.
/// </summary>
public class ModernUploadPanel : Panel
{
private readonly ModernButton _uploadBtn;
private readonly ModernButton _downloadBtn;
private ThemeColors _theme;
private string _title = "Загрузка прайс-листа и синхронизация";
private string _subtitle = "Импорт PriceList.xml или обмен данными с сервером поставщика.";
private string _title = "Синхронизация прайс-листа";
private string _subtitle = "Обмен данными с сервером: скачайте актуальный прайс или отправьте локальные изменения.";
public ModernButton UploadButton => _uploadBtn;
public ModernButton DownloadButton => _downloadBtn;
@ -47,16 +47,18 @@ namespace Elfisa.UI.Controls
Name = "uploadBtn",
Text = "Загрузить на сервер",
ButtonStyle = ModernButtonStyle.Primary,
Width = 220,
Height = 40
Width = 200,
Height = 40,
AllowTextEllipsis = false
};
_downloadBtn = new ModernButton
{
Name = "downloadBtn",
Text = "Скачать с сервера",
ButtonStyle = ModernButtonStyle.Secondary,
Width = 220,
Height = 40
Width = 200,
Height = 40,
AllowTextEllipsis = false
};
Controls.Add(_downloadBtn);
@ -78,7 +80,7 @@ namespace Elfisa.UI.Controls
var card = GetCardRect();
var totalWidth = _uploadBtn.Width + 12 + _downloadBtn.Width;
var left = card.X + Math.Max(24, (card.Width - totalWidth) / 2);
var top = card.Bottom - 64;
var top = card.Y + 168;
_uploadBtn.Location = new Point(left, top);
_downloadBtn.Location = new Point(left + _uploadBtn.Width + 12, top);
@ -86,12 +88,15 @@ namespace Elfisa.UI.Controls
private Rectangle GetCardRect()
{
var margin = 20;
const int cardW = 560;
const int cardH = 240;
var w = Math.Min(cardW, Math.Max(320, Width - 48));
var h = Math.Min(cardH, Math.Max(200, Height - 48));
return new Rectangle(
margin,
margin,
Math.Max(200, Width - margin * 2),
Math.Max(200, Height - margin * 2));
Math.Max(24, (Width - w) / 2),
Math.Max(24, (Height - h) / 2),
w,
h);
}
protected override void OnPaint(PaintEventArgs e)
@ -102,44 +107,52 @@ namespace Elfisa.UI.Controls
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
var card = GetCardRect();
using (var fill = new SolidBrush(theme.AccentPrimarySoft))
using (var shadow = new SolidBrush(Color.FromArgb(18, theme.TextPrimary)))
{
GraphicsExtensions.FillRoundedRectangle(
g,
shadow,
new Rectangle(card.X + 3, card.Y + 4, card.Width, card.Height),
theme.CornerRadiusMedium);
}
using (var fill = new SolidBrush(theme.Surface))
{
GraphicsExtensions.FillRoundedRectangle(g, fill, card, theme.CornerRadiusMedium);
}
using (var pen = new Pen(theme.AccentPrimaryBorder, 2f))
using (var pen = new Pen(theme.Border, 1f))
{
pen.DashStyle = DashStyle.Dash;
GraphicsExtensions.DrawRoundedRectangle(g, pen, card, theme.CornerRadiusMedium);
}
var iconRect = new Rectangle(card.X + (card.Width - 40) / 2, card.Y + 28, 40, 40);
using (var iconBrush = new SolidBrush(Color.FromArgb(40, theme.Success)))
var iconRect = new Rectangle(card.X + (card.Width - 44) / 2, card.Y + 28, 44, 44);
using (var iconBrush = new SolidBrush(theme.AccentPrimarySoft))
{
GraphicsExtensions.FillRoundedRectangle(g, iconBrush, iconRect, 10);
GraphicsExtensions.FillRoundedRectangle(g, iconBrush, iconRect, 12);
}
using (var iconFont = new Font("Segoe MDL2 Assets", 20f))
using (var iconFont = new Font("Segoe MDL2 Assets", 18f))
{
TextRenderer.DrawText(
g,
"\uE896",
iconFont,
iconRect,
theme.Success,
theme.AccentPrimary,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine);
}
var titleRect = new Rectangle(card.X + 24, card.Y + 78, card.Width - 48, 32);
var titleRect = new Rectangle(card.X + 28, card.Y + 84, card.Width - 56, 28);
TextRenderer.DrawText(
g,
_title,
theme.FontSemibold16,
titleRect,
theme.Success,
theme.TextPrimary,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
var subRect = new Rectangle(card.X + 32, card.Y + 112, card.Width - 64, 48);
var subRect = new Rectangle(card.X + 36, card.Y + 116, card.Width - 72, 44);
TextRenderer.DrawText(
g,
_subtitle,

View File

@ -1,11 +1,12 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using Elfisa.UI.Controls;
using Elfisa.UI.Theming;
namespace Elfisa.UI.Helpers
{
/// <summary>
/// Градиент из skill winforms-modern-buttons для ModernButton (без тяжёлой тени — она давала артефакты на toolbar).
/// Градиент для Primary/Danger в тон активной палитры (Clinical Pro teal, не «Google blue»).
/// </summary>
internal static class SkillGradientButtonPaint
{
@ -26,41 +27,45 @@ namespace Elfisa.UI.Helpers
out Color top,
out Color bottom)
{
var theme = ThemeManager.Colors;
if (style == ModernButtonStyle.Danger)
{
var danger = theme.Danger;
if (pressed)
{
top = Color.FromArgb(175, 30, 30);
bottom = Color.FromArgb(140, 20, 20);
top = GraphicsExtensions.Blend(danger, Color.Black, 0.22f);
bottom = GraphicsExtensions.Blend(danger, Color.Black, 0.32f);
}
else if (hover)
{
top = Color.FromArgb(245, 110, 110);
bottom = Color.FromArgb(215, 55, 55);
top = GraphicsExtensions.Blend(danger, Color.White, 0.18f);
bottom = danger;
}
else
{
top = Color.FromArgb(235, 87, 87);
bottom = Color.FromArgb(200, 40, 40);
top = GraphicsExtensions.Blend(danger, Color.White, 0.08f);
bottom = GraphicsExtensions.Blend(danger, Color.Black, 0.12f);
}
return;
}
// Primary — мягкий teal шапки, без синего «крика».
if (pressed)
{
top = Color.FromArgb(25, 70, 160);
bottom = Color.FromArgb(15, 55, 130);
top = theme.AccentPrimaryPressed;
bottom = GraphicsExtensions.Blend(theme.AccentPrimaryPressed, Color.Black, 0.12f);
}
else if (hover)
{
top = Color.FromArgb(90, 150, 255);
bottom = Color.FromArgb(45, 105, 220);
top = theme.AccentPrimaryHover;
bottom = theme.AccentPrimary;
}
else
{
top = Color.FromArgb(66, 133, 244);
bottom = Color.FromArgb(30, 87, 194);
top = theme.TitleBarBottom;
bottom = theme.TitleBarTop;
}
}
@ -73,6 +78,7 @@ namespace Elfisa.UI.Helpers
bool pressed,
bool enabled)
{
var theme = ThemeManager.Colors;
var radius = GetCornerRadius(style, compactIconMode);
var buttonRect = new Rectangle(
clientRect.X,
@ -86,7 +92,7 @@ namespace Elfisa.UI.Helpers
if (!enabled)
{
using (var brush = new SolidBrush(Color.FromArgb(220, 230, 235)))
using (var brush = new SolidBrush(GraphicsExtensions.Blend(theme.Border, theme.Surface, 0.35f)))
{
GraphicsExtensions.FillRoundedRectangle(g, brush, buttonRect, radius);
}

View File

@ -46,7 +46,7 @@ namespace Elfisa.UI.Helpers
TrySetInt(form.Handle, DwmwaUseImmersiveDarkMode, useDarkMode);
TrySetInt(form.Handle, DwmwaUseImmersiveDarkModeLegacy, useDarkMode);
var caption = theme.TitleBarTop;
var caption = theme.AccentPrimary;
var text = IsLightCaption(caption) ? Color.Black : Color.White;
TrySetUInt(form.Handle, DwmwaCaptionColor, ColorToDwm(caption));
@ -55,6 +55,41 @@ namespace Elfisa.UI.Helpers
RefreshNonClientArea(form);
}
/// <summary>
/// Обработка ресайза для FormBorderStyle.None (края окна).
/// </summary>
public static bool TryHandleNcHitTest(Form form, ref Message m, int border = 6)
{
const int wmNcHitTest = 0x84;
if (m.Msg != wmNcHitTest || form == null)
{
return false;
}
var screen = new Point(m.LParam.ToInt32() & 0xFFFF, (m.LParam.ToInt32() >> 16) & 0xFFFF);
// signed short for multi-monitor negative coords
screen = new Point((short)(m.LParam.ToInt32() & 0xFFFF), (short)((m.LParam.ToInt32() >> 16) & 0xFFFF));
var client = form.PointToClient(screen);
var w = form.ClientSize.Width;
var h = form.ClientSize.Height;
var left = client.X <= border;
var right = client.X >= w - border;
var top = client.Y <= border;
var bottom = client.Y >= h - border;
if (top && left) { m.Result = (IntPtr)13; return true; } // HTTOPLEFT
if (top && right) { m.Result = (IntPtr)14; return true; } // HTTOPRIGHT
if (bottom && left) { m.Result = (IntPtr)16; return true; } // HTBOTTOMLEFT
if (bottom && right) { m.Result = (IntPtr)17; return true; } // HTBOTTOMRIGHT
if (left) { m.Result = (IntPtr)10; return true; } // HTLEFT
if (right) { m.Result = (IntPtr)11; return true; } // HTRIGHT
if (top) { m.Result = (IntPtr)12; return true; } // HTTOP
if (bottom) { m.Result = (IntPtr)15; return true; } // HTBOTTOM
return false;
}
public static void ApplyThemedDialogHeader(Panel headerPanel, Label headerLabel, ThemeColors theme)
{
if (theme == null)

View File

@ -26,57 +26,57 @@ namespace Elfisa.UI.Theming
AccentPrimary = Color.FromArgb(15, 118, 110),
AccentPrimaryHover = Color.FromArgb(13, 148, 136),
AccentPrimaryPressed = Color.FromArgb(17, 94, 89),
AccentPrimarySoft = Color.FromArgb(236, 253, 245),
AccentPrimarySoft = Color.FromArgb(240, 253, 250),
AccentPrimaryBorder = Color.FromArgb(153, 246, 228),
AccentSecondary = Color.White,
Success = Color.FromArgb(5, 150, 105),
SuccessSoft = Color.FromArgb(209, 250, 229),
Warning = Color.FromArgb(217, 119, 6),
WarningSoft = Color.FromArgb(254, 243, 199),
Danger = Color.FromArgb(225, 29, 72),
DangerSoft = Color.FromArgb(255, 241, 242),
Danger = Color.FromArgb(190, 58, 82),
DangerSoft = Color.FromArgb(255, 245, 247),
Info = Color.FromArgb(8, 145, 178),
InfoSoft = Color.FromArgb(224, 242, 254),
TitleBarTop = Color.FromArgb(17, 94, 89),
TitleBarBottom = Color.FromArgb(15, 118, 110),
TitleBarTop = Color.FromArgb(15, 118, 110),
TitleBarBottom = Color.FromArgb(13, 148, 136),
NavBackground = Color.White,
CartHeaderBackground = Color.FromArgb(236, 253, 245),
ToolbarBackground = Color.FromArgb(252, 253, 253),
CartHeaderBackground = Color.FromArgb(240, 253, 250),
ToolbarBackground = Color.FromArgb(248, 250, 252),
SectionHeaderBackground = Color.FromArgb(241, 245, 249),
SidebarBackground = Color.White,
SidebarItem = Color.FromArgb(100, 116, 139),
SidebarItemHover = Color.FromArgb(248, 250, 252),
SidebarItemActive = Color.FromArgb(236, 253, 245),
SidebarItemActive = Color.FromArgb(204, 251, 241),
SidebarItemActiveText = Color.FromArgb(15, 118, 110),
GridHeaderBackground = Color.FromArgb(241, 245, 249),
GridHeaderForeground = Color.FromArgb(100, 116, 139),
GridHeaderForeground = Color.FromArgb(71, 85, 105),
GridRowBackground = Color.White,
GridRowAlternate = Color.FromArgb(248, 250, 252),
GridRowHover = Color.FromArgb(248, 250, 252),
GridRowHover = Color.FromArgb(240, 253, 250),
GridRowSelected = Color.FromArgb(15, 118, 110),
GridRowSelectedText = Color.White,
GridBorder = Color.FromArgb(241, 245, 249),
GridBorder = Color.FromArgb(226, 232, 240),
InputBackground = Color.White,
InputBorder = Color.FromArgb(226, 232, 240),
InputBorder = Color.FromArgb(203, 213, 225),
InputBorderFocus = Color.FromArgb(20, 184, 166),
OverlayScrim = Color.FromArgb(160, 15, 23, 42),
Shadow = Color.FromArgb(28, 15, 23, 42),
ScrollbarThumb = Color.FromArgb(203, 213, 225),
ScrollbarThumb = Color.FromArgb(148, 163, 184),
ScrollbarTrack = Color.FromArgb(248, 250, 252),
CornerRadiusSmall = 8,
CornerRadiusMedium = 10,
GridRowHeight = 38,
GridHeaderHeight = 40
CornerRadiusMedium = 12,
GridRowHeight = 36,
GridHeaderHeight = 38
};
public ClinicalProPalette()
{
Colors.FontRegular12 = new Font("Segoe UI", 12f);
Colors.FontRegular13 = new Font("Segoe UI", 13f);
Colors.FontRegular14 = new Font("Segoe UI", 14f);
Colors.FontSemibold14 = new Font("Segoe UI Semibold", 14f);
Colors.FontSemibold16 = new Font("Segoe UI Semibold", 16f);
Colors.FontBold18 = new Font("Segoe UI", 18f, FontStyle.Bold);
Colors.FontRegular12 = new Font("Segoe UI", 11f);
Colors.FontRegular13 = new Font("Segoe UI", 12f);
Colors.FontRegular14 = new Font("Segoe UI", 13f);
Colors.FontSemibold14 = new Font("Segoe UI Semibold", 13f);
Colors.FontSemibold16 = new Font("Segoe UI Semibold", 15f);
Colors.FontBold18 = new Font("Segoe UI Semibold", 17f, FontStyle.Bold);
}
}
}

View File

@ -17,7 +17,7 @@ namespace Elfisa.UI.Theming
{ ThemeVariant.DarkProductivity, new DarkProductivityPalette() }
};
private static ThemeVariant _currentVariant = ThemeVariant.FluentWin11;
private static ThemeVariant _currentVariant = ThemeVariant.ClinicalPro;
public static event EventHandler ThemeChanged;
@ -69,7 +69,7 @@ namespace Elfisa.UI.Theming
public static void ToggleProductivityTheme()
{
CurrentVariant = IsDarkProductivityActive
? ThemeVariant.PharmaTrust
? ThemeVariant.ClinicalPro
: ThemeVariant.DarkProductivity;
}
}