SKLADm/Form1.cs
2025-05-01 22:46:44 +07:00

500 lines
28 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography; // Для шифрования/дешифрования
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SKLADm.Properties; // Для доступа к Settings.Default
using SKLADm;
using ZXing;
using ZXing.Common;
using ZXing.Windows.Compatibility;
namespace OzonInternalLabelPrinter // Убедитесь, что это ваше пространство имен
{
public partial class Form1 : MaterialSkin.Controls.MaterialForm // <-- Должно быть так
{
private HttpClient _httpClient;
// Bitmap больше не нужен как поле класса
// private Bitmap _labelBitmap;
private string _currentProductName;
private string _currentSku;
private string _currentBarcode;
private List<OzonStore> _availableStores;
// Поле для хранения экземпляра документа между настройкой и печатью
private PrintDocument _printDocForSetup; // Используем для PageSetup и Print
// Конструктор формы
public Form1()
{
InitializeComponent(); // Инициализирует компоненты из ДИЗАЙНЕРА
InitializeHttpClient();
// Инициализируем PrintDocument один раз
_printDocForSetup = new PrintDocument();
// Устанавливаем обработчик печати один раз
_printDocForSetup.PrintPage += new PrintPageEventHandler(pd_PrintPage);
// Загружаем магазины при запуске ПОСЛЕ инициализации _printDocForSetup
LoadStoresToComboBox();
}
// Инициализация HTTP клиента
private void InitializeHttpClient()
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api-seller.ozon.ru/")
};
_httpClient.DefaultRequestHeaders.Accept.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
// --- Логика работы с магазинами и ключами ---
private static readonly byte[] s_entropy = null; // Энтропия для шифрования
// Дешифрование Api-Key
private string DecryptApiKey(string encryptedKeyBase64)
{
if (string.IsNullOrEmpty(encryptedKeyBase64)) return string.Empty;
try
{
byte[] encryptedBytes = Convert.FromBase64String(encryptedKeyBase64);
byte[] decryptedBytes = ProtectedData.Unprotect(encryptedBytes, s_entropy, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(decryptedBytes);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка дешифрования ключа: {ex.Message}");
MessageBox.Show($"Не удалось дешифровать Api-Key для магазина '{cmbStores.Text}'. Возможно, настройки повреждены или созданы под другим пользователем.\n\nОшибка: {ex.Message}",
"Ошибка ключа", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
// Загрузка магазинов из настроек в ComboBox
private void LoadStoresToComboBox()
{
string json = Settings.Default.SavedStoresJson;
_availableStores = new List<OzonStore>();
if (!string.IsNullOrEmpty(json))
{
try
{
_availableStores = JsonConvert.DeserializeObject<List<OzonStore>>(json) ?? new List<OzonStore>();
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка загрузки списка магазинов: {ex.Message}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_availableStores = new List<OzonStore>();
}
}
cmbStores.DataSource = null;
cmbStores.DataSource = _availableStores;
cmbStores.DisplayMember = "StoreName";
bool storesExist = _availableStores.Any();
cmbStores.Enabled = storesExist;
txtOfferId.Enabled = storesExist;
btnPageSetup.Enabled = storesExist; // Включаем настройку, если есть магазины
// Кнопка GetData включится при вводе артикула
if (storesExist)
{
cmbStores.SelectedIndex = 0;
// Установим принтер по умолчанию для _printDocForSetup, если возможно
TrySetDefaultPrinterForDocument();
}
else
{
cmbStores.SelectedIndex = -1;
// Показываем сообщение, если нужно
// MessageBox.Show("Нет сохраненных магазинов...", "Внимание", MessageBoxButtons.OK, Information);
}
ResetProductData();
// Обновляем состояние кнопки GetData после загрузки
txtOfferId_TextChanged(txtOfferId, EventArgs.Empty);
}
// Попытка установить принтер по умолчанию для PrintDocument
private void TrySetDefaultPrinterForDocument()
{
string thermalPrinterName = "Xprinter XP-365B"; // Или другое имя по умолчанию
foreach (string printerName in PrinterSettings.InstalledPrinters)
{
if (printerName.Equals(thermalPrinterName, StringComparison.OrdinalIgnoreCase))
{
try
{
_printDocForSetup.PrinterSettings.PrinterName = printerName;
System.Diagnostics.Debug.WriteLine($"Принтер по умолчанию '{printerName}' установлен для PrintDocument.");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка установки принтера по умолчанию '{printerName}': {ex.Message}");
}
return; // Выходим после нахождения
}
}
System.Diagnostics.Debug.WriteLine($"Принтер по умолчанию '{thermalPrinterName}' не найден.");
}
// --- Обработчики событий UI ---
private void btnManageStores_Click(object sender, EventArgs e)
{
using (FormManageStores manageForm = new FormManageStores())
{
manageForm.ShowDialog(this);
LoadStoresToComboBox(); // Перезагружаем список
}
}
private void cmbStores_SelectedIndexChanged(object sender, EventArgs e)
{
ResetProductData();
txtOfferId_TextChanged(txtOfferId, EventArgs.Empty); // Обновляем состояние кнопки GetData
// Можно попробовать обновить принтер по умолчанию для _printDocForSetup,
// если у разных магазинов могут быть разные принтеры (но это усложнит логику)
}
private void txtOfferId_TextChanged(object sender, EventArgs e)
{
btnGetData.Enabled = cmbStores.SelectedItem != null && !string.IsNullOrEmpty(txtOfferId.Text.Trim());
if (string.IsNullOrEmpty(txtOfferId.Text.Trim()))
{
ResetProductData();
}
}
// --- Кнопка "Настройка принтера" ---
private void btnPageSetup_Click(object sender, EventArgs e)
{
// Убедимся, что принтер в _printDocForSetup актуален (если не нашли Xprinter при запуске,
// или если у пользователя несколько принтеров)
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(_printDocForSetup.PrinterSettings.PrinterName))
{
// Если текущий принтер недоступен, сбрасываем на принтер по умолчанию Windows
try
{
_printDocForSetup.PrinterSettings = new PrinterSettings(); // Сброс на настройки по умолчанию
System.Diagnostics.Debug.WriteLine("Текущий принтер в PrintDocument был недоступен, сброшен на принтер по умолчанию Windows.");
}
catch { }
}
pageSetupDialog1.Document = _printDocForSetup; // Связываем диалог с нашим PrintDocument
pageSetupDialog1.MinMargins = new Margins(0, 0, 0, 0); // Минимальные поля
pageSetupDialog1.AllowMargins = true; // Разрешаем менять поля
pageSetupDialog1.AllowOrientation = false; // Запрещаем менять ориентацию
pageSetupDialog1.AllowPaper = true; // Разрешаем менять размер бумаги
pageSetupDialog1.AllowPrinter = true; // Разрешаем выбирать другой принтер
if (pageSetupDialog1.ShowDialog(this) == DialogResult.OK)
{
// Настройки применились к _printDocForSetup
lblStatus.Text = "Настройки принтера применены.";
System.Diagnostics.Debug.WriteLine($"PageSetupDialog OK: New PaperSize={_printDocForSetup.DefaultPageSettings.PaperSize.PaperName}, Printer={_printDocForSetup.PrinterSettings.PrinterName}");
}
else
{
lblStatus.Text = "Настройка принтера отменена.";
}
}
// --- Получение данных из API ---
private async void btnGetData_Click(object sender, EventArgs e)
{
OzonStore selectedStore = cmbStores.SelectedItem as OzonStore;
if (selectedStore == null) { MessageBox.Show("Выберите магазин.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
string clientId = selectedStore.ClientId;
string apiKey = DecryptApiKey(selectedStore.EncryptedApiKey);
if (apiKey == null) { lblStatus.Text = "Ошибка ключа API."; return; }
string rawOfferId = txtOfferId.Text.Trim();
if (string.IsNullOrEmpty(rawOfferId)) { MessageBox.Show("Введите Артикул.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
string offerId = Regex.Replace(rawOfferId, @"[^a-zA-Zа-яА-Я0-9_-]", "");
if (string.IsNullOrEmpty(offerId)) { MessageBox.Show("Артикул некорректен.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
if (offerId != rawOfferId) { txtOfferId.Text = offerId; }
ResetProductData();
lblStatus.Text = "Запрос данных...";
Application.DoEvents();
_httpClient.DefaultRequestHeaders.Remove("Client-Id");
_httpClient.DefaultRequestHeaders.Remove("Api-Key");
_httpClient.DefaultRequestHeaders.Add("Client-Id", clientId);
_httpClient.DefaultRequestHeaders.Add("Api-Key", apiKey);
string responseBody = string.Empty;
try
{
var requestData = new { offer_id = new[] { offerId } };
var jsonRequest = JsonConvert.SerializeObject(requestData);
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
HttpResponseMessage response = await _httpClient.PostAsync("/v3/product/info/list", content);
responseBody = await response.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine($"API Response ({response.StatusCode}): {responseBody}");
if (response.IsSuccessStatusCode)
{
JObject jsonResponse = JObject.Parse(responseBody);
JToken itemsArray = jsonResponse["items"];
if (itemsArray != null && itemsArray.Type == JTokenType.Array && itemsArray.HasValues)
{
JToken firstItem = itemsArray[0];
if (firstItem != null)
{
_currentProductName = firstItem["name"]?.ToString() ?? "N/A";
_currentSku = firstItem["offer_id"]?.ToString() ?? offerId;
_currentBarcode = firstItem["barcode"]?.ToString();
if (string.IsNullOrEmpty(_currentBarcode))
{
JToken barcodesToken = firstItem["barcodes"];
if (barcodesToken != null && barcodesToken.Type == JTokenType.Array && barcodesToken.HasValues)
{ _currentBarcode = barcodesToken.First?.ToString(); }
}
_currentBarcode = _currentBarcode ?? "N/A";
GenerateLabelPreview(); // Обновляем UI и включаем кнопку печати
}
else { HandleApiError("Нет данных о товаре в ответе.", responseBody); }
}
else { HandleApiError("Ответ API не содержит списка товаров.", responseBody); }
}
else { HandleApiError($"Ошибка API: {response.StatusCode}", responseBody); }
}
catch (JsonReaderException jsonEx) { MessageBox.Show($"Ошибка JSON: {jsonEx.Message}\n{responseBody}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); ResetProductData(); }
catch (HttpRequestException httpEx) { MessageBox.Show($"Ошибка сети: {httpEx.Message}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); ResetProductData(); }
catch (Exception ex) { MessageBox.Show($"Ошибка: {ex.Message}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); ResetProductData(); }
finally { apiKey = null; }
}
// Обработка ошибок API
private void HandleApiError(string messagePrefix, string responseBody)
{
string errorMessage = $"{messagePrefix}";
try
{
JObject errorJson = JObject.Parse(responseBody);
string ozonError = errorJson["message"]?.ToString() ?? errorJson["error"]?["message"]?.ToString();
errorMessage = string.IsNullOrEmpty(ozonError) ? $"{messagePrefix}\n{responseBody}" : $"{messagePrefix}\n{ozonError}";
}
catch { errorMessage = $"{messagePrefix}\n{responseBody}"; }
MessageBox.Show(errorMessage, "Ошибка API Ozon", MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "Ошибка API.";
ResetProductData();
}
// Сброс данных о товаре и состояния UI
private void ResetProductData()
{
_currentProductName = null;
_currentSku = null;
_currentBarcode = null;
lblProductName.Text = "Название:";
lblProductSku.Text = "Артикул:";
lblProductBarcode.Text = "Штрихкод:";
// Очищаем превью, если оно есть
if (this.Controls.ContainsKey("picLabelPreview"))
(this.Controls["picLabelPreview"] as PictureBox).Image = null;
btnPrintLabel.Enabled = false;
// lblStatus.Text = "Статус:";
}
// Генерация (только обновление UI)
private void GenerateLabelPreview()
{
bool dataAvailable = !string.IsNullOrEmpty(_currentSku) &&
!string.IsNullOrEmpty(_currentProductName) &&
!string.IsNullOrEmpty(_currentBarcode) &&
_currentBarcode != "N/A";
if (!dataAvailable) { ResetProductData(); return; }
lblProductName.Text = $"Название: {_currentProductName}";
lblProductSku.Text = $"Артикул: {_currentSku}";
lblProductBarcode.Text = $"Штрихкод: {_currentBarcode}";
// Очистка превью, если оно есть
if (this.Controls.ContainsKey("picLabelPreview"))
(this.Controls["picLabelPreview"] as PictureBox).Image = null;
btnPrintLabel.Enabled = true;
lblStatus.Text = "Данные получены, готово к печати.";
}
// --- Печать ---
// Кнопка "Печать этикетки"
private void btnPrintLabel_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentSku) || string.IsNullOrEmpty(_currentProductName) || string.IsNullOrEmpty(_currentBarcode) || _currentBarcode == "N/A")
{
MessageBox.Show("Нет данных для печати.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
PrintDialog printDialog = new PrintDialog();
printDialog.Document = _printDocForSetup; // Используем настроенный документ
printDialog.AllowSomePages = false;
printDialog.AllowSelection = false;
printDialog.UseEXDialog = true;
if (printDialog.ShowDialog(this) == DialogResult.OK)
{
// _printDocForSetup уже содержит принтер и настройки страницы, выбранные в PrintDialog или PageSetupDialog
// Переустанавливаем поля на случай, если PrintDialog их сбросил
_printDocForSetup.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0);
_printDocForSetup.OriginAtMargins = false;
try
{
lblStatus.Text = $"Печать на {_printDocForSetup.PrinterSettings.PrinterName}...";
_printDocForSetup.Print(); // Печатаем с текущими настройками
lblStatus.Text = "Отправлено на печать.";
}
catch (InvalidPrinterException)
{
MessageBox.Show($"Ошибка: Принтер '{_printDocForSetup.PrinterSettings.PrinterName}' недоступен или не найден.\nПроверьте подключение или выберите другой принтер в настройках.", "Ошибка принтера", MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "Ошибка принтера.";
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка печати: {ex.Message}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "Ошибка печати.";
}
}
else { lblStatus.Text = "Печать отменена."; }
printDialog.Dispose();
}
// Метод отрисовки страницы для печати (динамический)
private void pd_PrintPage(object sender, PrintPageEventArgs e)
{
// Проверка данных
if (string.IsNullOrEmpty(_currentSku) || string.IsNullOrEmpty(_currentProductName) || string.IsNullOrEmpty(_currentBarcode) || _currentBarcode == "N/A")
{ System.Diagnostics.Debug.WriteLine("PrintPage ABORTED - Missing data!"); e.Cancel = true; return; }
Graphics g = e.Graphics;
// Получаем РЕАЛЬНЫЕ границы печатаемой области в единицах Graphics (обычно сотые дюйма)
// Не PageBounds, а PrintableArea! PageBounds - это физический размер листа.
RectangleF printArea = e.PageSettings.PrintableArea;
// Отступы от КРАЕВ ПЕЧАТАЕМОЙ ОБЛАСТИ (в единицах Graphics)
// Подбирайте эти значения, чтобы получить рамку внутри печатаемой зоны
float marginX = 5 * g.DpiX / 100f; // Пример: 5/100 дюйма -> в единицах Graphics
float marginY = 3 * g.DpiY / 100f; // Пример: 3/100 дюйма -> в единицах Graphics
// Область для рисования контента внутри отступов
float drawableX = printArea.Left + marginX;
float drawableY = printArea.Top + marginY;
float drawableWidth = printArea.Width - (marginX * 2);
float drawableHeight = printArea.Height - (marginY * 2);
float currentY = drawableY; // Текущая позиция по Y
// Проверка на валидность области
if (drawableWidth <= 0 || drawableHeight <= 0) { System.Diagnostics.Debug.WriteLine("PrintPage ABORTED - Invalid drawable area!"); e.Cancel = true; return; }
System.Diagnostics.Debug.WriteLine("--- Printing Page (Dynamic) ---");
System.Diagnostics.Debug.WriteLine($"Paper: {e.PageSettings.PaperSize.PaperName} ({e.PageSettings.PaperSize.Width}x{e.PageSettings.PaperSize.Height}) hund.inch");
System.Diagnostics.Debug.WriteLine($"PrintableArea ({g.PageUnit}): X={printArea.X:F2}, Y={printArea.Y:F2}, W={printArea.Width:F2}, H={printArea.Height:F2}");
System.Diagnostics.Debug.WriteLine($"Drawable Area ({g.PageUnit}): X={drawableX:F2}, Y={drawableY:F2}, W={drawableWidth:F2}, H={drawableHeight:F2}");
// Шрифты (размер в пунктах)
float baseFontSizePoints = 6f; // Базовый размер, можно менять
Font titleFont = new Font("Arial", baseFontSizePoints + 1, FontStyle.Bold);
Font regularFont = new Font("Arial", baseFontSizePoints);
Font barcodeTextFont = new Font("Arial", baseFontSizePoints - 1);
try
{
// --- Рисуем Название ---
string productName = _currentProductName ?? "N/A";
SizeF titleSize = g.MeasureString(productName, titleFont, (int)drawableWidth);
// Ограничиваем высоту, чтобы поместилось остальное
float maxTitleHeight = drawableHeight * 0.3f;
float actualTitleHeight = Math.Min(titleSize.Height, maxTitleHeight);
g.DrawString(productName, titleFont, Brushes.Black, new RectangleF(drawableX, currentY, drawableWidth, actualTitleHeight));
currentY += actualTitleHeight + (2 * g.DpiY / 72f); // Отступ 2 пункта
// --- Рисуем Артикул ---
string skuText = $"Артикул: {_currentSku ?? "N/A"}";
SizeF skuSize = g.MeasureString(skuText, regularFont, (int)drawableWidth);
if (currentY + skuSize.Height < printArea.Bottom - marginY - 15 * 100f / g.DpiY) // Оставляем ~15px под ШК + текст
{
g.DrawString(skuText, regularFont, Brushes.Black, drawableX, currentY);
currentY += skuSize.Height + (3 * g.DpiY / 72f); // Отступ 3 пункта
}
// --- Рисуем Штрихкод ---
float remainingHeight = (printArea.Bottom - marginY) - currentY; // Оставшаяся высота
if (remainingHeight > 15 * 100f / g.DpiY) // Если осталось хотя бы ~15px
{
try
{
// --- Расчет размеров ШК ---
float barcodeTextHeight = g.MeasureString(_currentBarcode, barcodeTextFont).Height;
float barcodePrintHeight = Math.Max(10 * 100f / g.DpiY, remainingHeight - barcodeTextHeight - (2 * g.DpiY / 72f)); // Вычитаем текст и отступ
float barcodePrintWidth = drawableWidth * 0.98f;
int barcodePixelHeight = (int)(barcodePrintHeight * g.DpiY / 100f);
int barcodePixelWidth = (int)(barcodePrintWidth * g.DpiX / 100f);
if (barcodePixelHeight < 10 || barcodePixelWidth < 40) throw new Exception("Слишком мало места для ШК.");
BarcodeFormat barcodeFormat = BarcodeFormat.CODE_128;
// ... (определение barcodeFormat) ...
if (_currentBarcode.Length == 13 && long.TryParse(_currentBarcode, out _)) barcodeFormat = BarcodeFormat.EAN_13;
else if (_currentBarcode.Length == 8 && long.TryParse(_currentBarcode, out _)) barcodeFormat = BarcodeFormat.EAN_8;
var barcodeWriter = new ZXing.Windows.Compatibility.BarcodeWriter { Format = barcodeFormat, Options = new EncodingOptions { Height = barcodePixelHeight, Width = barcodePixelWidth, PureBarcode = true, Margin = 1 } };
using (var barcodeBitmap = barcodeWriter.Write(_currentBarcode))
{
float barcodeX = drawableX + (drawableWidth - barcodePrintWidth) / 2.0f; // Центрируем
g.DrawImage(barcodeBitmap, barcodeX, currentY, barcodePrintWidth, barcodePrintHeight);
currentY += barcodePrintHeight + (1 * g.DpiY / 72f); // Отступ 1 пункт
}
// --- Рисуем текст ШК ---
SizeF barcodeTextSize = g.MeasureString(_currentBarcode, barcodeTextFont);
if (currentY + barcodeTextSize.Height <= printArea.Bottom - marginY)
{
float textX = drawableX + (drawableWidth - barcodeTextSize.Width) / 2.0f; // Центрируем
g.DrawString(_currentBarcode, barcodeTextFont, Brushes.Black, textX, currentY);
}
else { System.Diagnostics.Debug.WriteLine("SKIPPING Barcode text - no space"); }
}
catch (Exception exBarcode)
{
System.Diagnostics.Debug.WriteLine($"Error drawing barcode: {exBarcode.Message}");
g.DrawString("Ошибка ШК", regularFont, Brushes.Red, drawableX, currentY); // Рисуем ошибку вместо ШК
}
}
else { System.Diagnostics.Debug.WriteLine("SKIPPING Barcode - not enough vertical space"); }
}
catch (Exception exDraw) { System.Diagnostics.Debug.WriteLine($"Error drawing page: {exDraw}"); }
finally { titleFont.Dispose(); regularFont.Dispose(); barcodeTextFont.Dispose(); }
e.HasMorePages = false;
}
} // Конец класса Form1
} // Конец namespace