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 _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(); if (!string.IsNullOrEmpty(json)) { try { _availableStores = JsonConvert.DeserializeObject>(json) ?? new List(); } catch (Exception ex) { MessageBox.Show($"Ошибка загрузки списка магазинов: {ex.Message}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning); _availableStores = new List(); } } 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().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