SKLADm/Form1.cs

500 lines
28 KiB
C#
Raw Normal View History

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