From a6550e99286bf50b6ecdaee490b952ba53207cfd Mon Sep 17 00:00:00 2001 From: nobobo Date: Mon, 1 Jun 2026 13:23:34 +0900 Subject: [PATCH] for Push. --- .gitattributes | 63 ++ .gitignore | 363 +++++++++ Alert/FullScreenAlertUxer.cs | 30 + Alert/Gamen/AlertForm.cs | 347 +++++++++ Alert/Gamen/AlertForm.resx | 123 +++ Alert/ToastUxer.cs | 34 + Alert/WindowFlasherUxer.cs | 38 + AutoTest/Command/Alias/CommandAliasUxer.cs | 22 + AutoTest/Command/Alias/Ini/IniReaderUxer.cs | 63 ++ .../Alias/Loader/CommandAliasLoaderUxer.cs | 27 + .../Command/Definition/CommandEnumUxer.cs | 23 + .../Command/Definition/CommandParserUxer.cs | 22 + .../Translator/CommandTranslatorUxer.cs | 23 + AutoTest/Core/StepTestRunnerUxer.cs | 31 + AutoTest/Core/TestModeInitializerUxer.cs | 64 ++ AutoTest/Core/TestModeUxer.cs | 18 + AutoTest/Script/Load/LoadTryUxer.cs | 91 +++ AutoTest/Script/Models/ParsedStepDTO.cs | 27 + AutoTest/Script/Models/StepDTO.cs | 27 + AutoTest/Script/Models/StepInfoDTO.cs | 19 + .../Script/Parse/Function/FunctionTryUxer.cs | 33 + AutoTest/Script/Parse/ParseTryUxer.cs | 47 ++ .../Script/Storage/TestScriptStorageUxer.cs | 16 + Batch/Cleanup/CleanupStepHandlerUxer.cs | 51 ++ Batch/Commons/CommonUxer.cs | 96 +++ Batch/DTO/StepDto.cs | 14 + Batch/DTO/StepInfoDto.cs | 14 + Batch/Dispatch/StepDispatcherUxer.cs | 56 ++ Batch/Exchange/CsvToDbStepHandlerUxer.cs | 357 +++++++++ Batch/IO/FileStepHandlerUxer.cs | 87 +++ Batch/Interface/IFuncResultDto.cs | 15 + Batch/Sftp/SftpStepHandlerUxer.cs | 246 ++++++ Batch/StepInfo/StepInfoUxer.cs | 54 ++ CL_設計別府条項.md | 29 + Cleanup/FileCleanupUxer.cs | 80 ++ Commons/Conversion/KanaConvertUxer.cs | 54 ++ Commons/DateHelperUxer.cs | 35 + Commons/DictionaryUxer.cs | 55 ++ Commons/EncodingUxer.cs | 54 ++ Commons/IntUxer.cs | 23 + Commons/LoggerUxer.cs | 81 ++ Commons/RetCodeUxer.cs | 137 ++++ Commons/StringUxer.cs | 104 +++ Commons/VariableUxer.cs | 16 + Communications/SftpUxer.cs | 302 ++++++++ .../Attributes/ExcelColumnLetterAttribute.cs | 25 + Encryption/FIleEncryptUxer.cs | 271 +++++++ Encryption/SimpleEncryptUxer.cs | 96 +++ Exchange/CsvDbSyncUxer.cs | 153 ++++ Exchange/Interfaces/ICsvExportableUxer.cs | 14 + Forms/ColorHelper.cs | 74 ++ Forms/ContextMenuFunction/FontUxer.cs | 74 ++ .../Gamen/DgvColumnEditorForm.Designer.cs | 197 +++++ .../Gamen/DgvColumnEditorForm.cs | 334 ++++++++ .../Gamen/DgvColumnEditorForm.resx | 132 ++++ Forms/ContextMenuHelper.cs | 332 ++++++++ Forms/Controls/PoiDgv.cs | 79 ++ Forms/Controls/PoiDgv.resx | 123 +++ Forms/Controls/PoiMainForm.cs | 588 ++++++++++++++ Forms/DataGridViewExporter.cs | 172 +++++ Forms/DatePickerHelper.cs | 58 ++ Forms/DgvHelper.cs | 726 ++++++++++++++++++ Forms/FormHelper.cs | 175 +++++ Forms/ToastNotifier.cs | 181 +++++ IO/Database/DbUxer.cs | 684 +++++++++++++++++ IO/Excel/ExcelCellValidatorUxer.cs | 60 ++ IO/Excel/ExcelColumnHelperUxer.cs | 26 + IO/Excel/XlUxer.cs | 95 +++ IO/File/CSVUxer.cs | 158 ++++ IO/File/FileUxer.cs | 141 ++++ IO/File/JsonFileUxer.cs | 206 +++++ IO/File/PathUxer.cs | 174 +++++ InBox/IFilterUxer.cs | 21 + InBox/LineFilterUxer.cs | 42 + InBox/SheetFilterUxer.cs | 72 ++ Ini/IniDto.cs | 15 + Ini/IniHelper.cs | 147 ++++ Ini/IniReaderUxer.cs | 55 ++ Ini/IniReferenceResolveUxer.cs | 75 ++ Input/Gamen/NumericInputForm.Designer.cs | 106 +++ Input/Gamen/NumericInputForm.cs | 41 + Input/Gamen/NumericInputForm.resx | 120 +++ Input/Gamen/TextInputForm.Designer.cs | 95 +++ Input/Gamen/TextInputForm.cs | 42 + Input/Gamen/TextInputForm.resx | 120 +++ KssSmaPlaLib.csproj | 233 ++++++ KssSmaPlaLib.sln | 25 + Properties/AssemblyInfo.cs | 40 + .../NamespaceAdjust/NamespaceAdjusterUxer.cs | 95 +++ Tools/FileHack/MergeSplitFilesUxer.cs | 63 ++ Tools/FileHack/SplitFileBySizeUxer.cs | 69 ++ app.config | 58 ++ ラッキイ青透過版(せり様).ico | Bin 0 -> 233150 bytes 93 files changed, 10393 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Alert/FullScreenAlertUxer.cs create mode 100644 Alert/Gamen/AlertForm.cs create mode 100644 Alert/Gamen/AlertForm.resx create mode 100644 Alert/ToastUxer.cs create mode 100644 Alert/WindowFlasherUxer.cs create mode 100644 AutoTest/Command/Alias/CommandAliasUxer.cs create mode 100644 AutoTest/Command/Alias/Ini/IniReaderUxer.cs create mode 100644 AutoTest/Command/Alias/Loader/CommandAliasLoaderUxer.cs create mode 100644 AutoTest/Command/Definition/CommandEnumUxer.cs create mode 100644 AutoTest/Command/Definition/CommandParserUxer.cs create mode 100644 AutoTest/Command/Translator/CommandTranslatorUxer.cs create mode 100644 AutoTest/Core/StepTestRunnerUxer.cs create mode 100644 AutoTest/Core/TestModeInitializerUxer.cs create mode 100644 AutoTest/Core/TestModeUxer.cs create mode 100644 AutoTest/Script/Load/LoadTryUxer.cs create mode 100644 AutoTest/Script/Models/ParsedStepDTO.cs create mode 100644 AutoTest/Script/Models/StepDTO.cs create mode 100644 AutoTest/Script/Models/StepInfoDTO.cs create mode 100644 AutoTest/Script/Parse/Function/FunctionTryUxer.cs create mode 100644 AutoTest/Script/Parse/ParseTryUxer.cs create mode 100644 AutoTest/Script/Storage/TestScriptStorageUxer.cs create mode 100644 Batch/Cleanup/CleanupStepHandlerUxer.cs create mode 100644 Batch/Commons/CommonUxer.cs create mode 100644 Batch/DTO/StepDto.cs create mode 100644 Batch/DTO/StepInfoDto.cs create mode 100644 Batch/Dispatch/StepDispatcherUxer.cs create mode 100644 Batch/Exchange/CsvToDbStepHandlerUxer.cs create mode 100644 Batch/IO/FileStepHandlerUxer.cs create mode 100644 Batch/Interface/IFuncResultDto.cs create mode 100644 Batch/Sftp/SftpStepHandlerUxer.cs create mode 100644 Batch/StepInfo/StepInfoUxer.cs create mode 100644 CL_設計別府条項.md create mode 100644 Cleanup/FileCleanupUxer.cs create mode 100644 Commons/Conversion/KanaConvertUxer.cs create mode 100644 Commons/DateHelperUxer.cs create mode 100644 Commons/DictionaryUxer.cs create mode 100644 Commons/EncodingUxer.cs create mode 100644 Commons/IntUxer.cs create mode 100644 Commons/LoggerUxer.cs create mode 100644 Commons/RetCodeUxer.cs create mode 100644 Commons/StringUxer.cs create mode 100644 Commons/VariableUxer.cs create mode 100644 Communications/SftpUxer.cs create mode 100644 DtoMapping/Attributes/ExcelColumnLetterAttribute.cs create mode 100644 Encryption/FIleEncryptUxer.cs create mode 100644 Encryption/SimpleEncryptUxer.cs create mode 100644 Exchange/CsvDbSyncUxer.cs create mode 100644 Exchange/Interfaces/ICsvExportableUxer.cs create mode 100644 Forms/ColorHelper.cs create mode 100644 Forms/ContextMenuFunction/FontUxer.cs create mode 100644 Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.Designer.cs create mode 100644 Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.cs create mode 100644 Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.resx create mode 100644 Forms/ContextMenuHelper.cs create mode 100644 Forms/Controls/PoiDgv.cs create mode 100644 Forms/Controls/PoiDgv.resx create mode 100644 Forms/Controls/PoiMainForm.cs create mode 100644 Forms/DataGridViewExporter.cs create mode 100644 Forms/DatePickerHelper.cs create mode 100644 Forms/DgvHelper.cs create mode 100644 Forms/FormHelper.cs create mode 100644 Forms/ToastNotifier.cs create mode 100644 IO/Database/DbUxer.cs create mode 100644 IO/Excel/ExcelCellValidatorUxer.cs create mode 100644 IO/Excel/ExcelColumnHelperUxer.cs create mode 100644 IO/Excel/XlUxer.cs create mode 100644 IO/File/CSVUxer.cs create mode 100644 IO/File/FileUxer.cs create mode 100644 IO/File/JsonFileUxer.cs create mode 100644 IO/File/PathUxer.cs create mode 100644 InBox/IFilterUxer.cs create mode 100644 InBox/LineFilterUxer.cs create mode 100644 InBox/SheetFilterUxer.cs create mode 100644 Ini/IniDto.cs create mode 100644 Ini/IniHelper.cs create mode 100644 Ini/IniReaderUxer.cs create mode 100644 Ini/IniReferenceResolveUxer.cs create mode 100644 Input/Gamen/NumericInputForm.Designer.cs create mode 100644 Input/Gamen/NumericInputForm.cs create mode 100644 Input/Gamen/NumericInputForm.resx create mode 100644 Input/Gamen/TextInputForm.Designer.cs create mode 100644 Input/Gamen/TextInputForm.cs create mode 100644 Input/Gamen/TextInputForm.resx create mode 100644 KssSmaPlaLib.csproj create mode 100644 KssSmaPlaLib.sln create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Tools/ClaraHack/NamespaceAdjust/NamespaceAdjusterUxer.cs create mode 100644 Tools/FileHack/MergeSplitFilesUxer.cs create mode 100644 Tools/FileHack/SplitFileBySizeUxer.cs create mode 100644 app.config create mode 100644 ラッキイ青透過版(せり様).ico diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/Alert/FullScreenAlertUxer.cs b/Alert/FullScreenAlertUxer.cs new file mode 100644 index 0000000..e3cd096 --- /dev/null +++ b/Alert/FullScreenAlertUxer.cs @@ -0,0 +1,30 @@ +using KssSmaPlaLib.Alert.Gamen; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Alert +{ + public static class FullScreenAlertUxer + { + public static void ShowFullScreenAlert( + Form orner, + string message, + DateTime dateTime, + bool kyoretsu = true, + int autoCloseSec = 0) + { + using (var alert = new AlertForm( + message, + dateTime, + kyoretsu, //true, + autoCloseSec)) + { + alert.ShowDialog(orner); // モーダルで前面確実化 + } + } + } +} diff --git a/Alert/Gamen/AlertForm.cs b/Alert/Gamen/AlertForm.cs new file mode 100644 index 0000000..fa97486 --- /dev/null +++ b/Alert/Gamen/AlertForm.cs @@ -0,0 +1,347 @@ +using KssSmaPlaLib.Ini; +using KssSmaPlaLib.Input.Gamen; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Alert.Gamen +{ + public class AlertForm : Form + { + private const string INI_KEY_BORDER = "Border"; + private const string INI_KEY_TITLE = "Title"; + private const string INI_KEY_ATTRIBUTE = "Attribute"; + + private readonly Timer _autoClose = new Timer(); + private readonly Label _title = new Label(); + private readonly Label _detail = new Label(); + private readonly Timer _pulse = new Timer(); + private ContextMenuStrip PopupMenu; + private IContainer components; + private ToolStripMenuItem BorderMenu; + private ToolStripMenuItem TitleMenu; + private ToolStripMenuItem ShozokuMenu; + private ToolStripMenuItem KyoretsuModeMenu; + private int _pulseStep = 0; + + /// + /// コンストラクタ + /// + /// ログのエラー行 + /// 日時 + /// 強烈画面 + /// 自動で閉じる秒数 + public AlertForm( + string errorLine, + DateTime dateTime, + bool kyouretsu = true, + int autoCloseSec = 0) + { + InitializeComponent(); + + if (kyouretsu || IniHelper.ReadValue(null, this.Name, "KyoretsuMode") == "1") + { + // ****** ここ本番強烈処理 ****** + IniHelper.WriteValue(null, this.Name, INI_KEY_BORDER, "1"); + if (string.IsNullOrEmpty(IniHelper.ReadValue(null, this.Name, INI_KEY_BORDER))) + { + FormBorderStyle = FormBorderStyle.None; + } + else + { + FormBorderStyle = FormBorderStyle.Sizable; + } + TopMost = true; + //WindowState = FormWindowState.Normal; + WindowState = FormWindowState.Maximized; + //Bounds = GetAllScreensBounds(); // 全画面(マルチモニタ) + // ****** ここ本番強烈処理 ****** + } + + BackColor = Color.FromArgb(220, 0, 0); // 深紅 + Cursor = Cursors.Hand; + MouseDown += All_MouseDown; + + // タイトル + _title.Text = IniHelper.ReadValue(null, this.Name, INI_KEY_TITLE, "(タイトル未設定)"); + var attribute = IniHelper.ReadValue(null, this.Name, INI_KEY_ATTRIBUTE, "のぼぼ工房"); + if(!string.IsNullOrEmpty(attribute)) + { + _title.Text += " - " + attribute; + } + _title.Font = new Font("Yu Gothic UI", 72, FontStyle.Bold); + _title.ForeColor = Color.White; + _title.AutoSize = true; //false; + _title.MaximumSize = new Size(Screen.PrimaryScreen.Bounds.Width, 0); // 左記Widthを超えると自動改行 + _title.TextAlign = ContentAlignment.MiddleCenter; + _title.Dock = DockStyle.Top; + // 高さはFormLoadにて変更する(フォームのHeightがまだデザイン時のものだから) + //_title.Height = Height / 3; + _title.MouseDown += All_MouseDown; + + // 詳細 + _detail.Text = $"[{dateTime:yyyy/MM/dd HH:mm:ss.fff}]\r\n{errorLine}"; + _detail.Font = new Font("Consolas", 28, FontStyle.Regular); + _detail.ForeColor = Color.White; + _detail.AutoSize = false; + _detail.TextAlign = ContentAlignment.MiddleCenter; + _detail.Dock = DockStyle.Fill; + _detail.MouseDown += All_MouseDown; + + Controls.Add(_detail); + Controls.Add(_title); + + // フェードイン風(パルスで脈動) + _pulse.Interval = 60; + _pulse.Tick += (_, __) => + { + _pulseStep++; + //int alpha = 200 + (int)(55 * Math.Sin(_pulseStep / 3.0)); + + //BackColor = Color.FromArgb(Math.Clamp(alpha, 180, 255), 120, 0, 0); // ほんのり血の脈動(控えめ) + // 180〜255にクランプ + //int a = Math.Min(255, Math.Max(180, alpha)); + //BackColor = Color.FromArgb(a, 120, 0, 0); // ほんのり血の脈動(控えめ) + int r = Math.Min(255, Math.Max(120, 120 + (int)(60 * Math.Sin(_pulseStep / 3.0)))); + BackColor = Color.FromArgb(255, r, 0, 0); + + //_title.ForeColor = (alpha % 2 == 0) ? Color.White : Color.GhostWhite; + _title.ForeColor = (r % 2 == 0) ? Color.White : Color.GhostWhite; + Invalidate(); + }; + _pulse.Start(); + + // 自動クローズ(10秒) + if (autoCloseSec != 0) + { + _autoClose.Interval = autoCloseSec * 1000; + _autoClose.Tick += (_, __) => Close(); + _autoClose.Start(); + } + + KeyPreview = true; + MouseDown += All_MouseDown; + Shown += AlertForm_Shown; + + // 前面に確実に + Activate(); + BringToFront(); + Focus(); + } + + private void AlertForm_Shown(object sender, EventArgs e) + { + // タスクバー点滅(アクティブになったらすぐ消えるが、ならなければずっと点滅) + WindowFlasherUxer.Flash(this.Handle); + } + + /// + /// フォームロード時 + /// + /// + /// + private void AlertForm_Load(object sender, EventArgs e) + { + // 高さはFormLoadにて変更する(コンストラクタではフォームのHeightがまだデザイン時のものだから) + _title.Height = Height / 3; + } + + // マルチスクリーンの時全てを足したサイズになる! + private Rectangle GetAllScreensBounds() + { + int minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + foreach (var s in Screen.AllScreens) + { + var b = s.Bounds; + minX = Math.Min(minX, b.Left); + minY = Math.Min(minY, b.Top); + maxX = Math.Max(maxX, b.Right); + maxY = Math.Max(maxY, b.Bottom); + } + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.PopupMenu = new System.Windows.Forms.ContextMenuStrip(this.components); + this.BorderMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.TitleMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.ShozokuMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.KyoretsuModeMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.PopupMenu.SuspendLayout(); + this.SuspendLayout(); + // + // PopupMenu + // + this.PopupMenu.Font = new System.Drawing.Font("メイリオ", 11F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(128))); + this.PopupMenu.ImageScalingSize = new System.Drawing.Size(24, 24); + this.PopupMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.BorderMenu, + this.TitleMenu, + this.ShozokuMenu, + this.KyoretsuModeMenu}); + this.PopupMenu.Name = "PopupMenu"; + this.PopupMenu.Size = new System.Drawing.Size(241, 197); + this.PopupMenu.Opening += new System.ComponentModel.CancelEventHandler(this.PopupMenu_Opening); + // + // BorderMenu + // + this.BorderMenu.Name = "BorderMenu"; + this.BorderMenu.Size = new System.Drawing.Size(240, 40); + this.BorderMenu.Text = "ボーダー..."; + this.BorderMenu.Click += new System.EventHandler(this.BorderMenu_Click); + // + // TitleMenu + // + this.TitleMenu.Name = "TitleMenu"; + this.TitleMenu.Size = new System.Drawing.Size(240, 40); + this.TitleMenu.Text = "タイトル..."; + this.TitleMenu.Click += new System.EventHandler(this.TitleMenu_Click); + // + // ShozokuMenu + // + this.ShozokuMenu.Name = "ShozokuMenu"; + this.ShozokuMenu.Size = new System.Drawing.Size(240, 40); + this.ShozokuMenu.Text = "所属..."; + this.ShozokuMenu.Click += new System.EventHandler(this.ShozokuMenu_Click); + // + // KyoretsuModeMenu + // + this.KyoretsuModeMenu.Name = "KyoretsuModeMenu"; + this.KyoretsuModeMenu.Size = new System.Drawing.Size(240, 40); + this.KyoretsuModeMenu.Text = "強烈モード?"; + this.KyoretsuModeMenu.Click += new System.EventHandler(this.KyoretsuModeMenu_Click); + // + // AlertForm + // + this.ClientSize = new System.Drawing.Size(278, 244); + this.ContextMenuStrip = this.PopupMenu; + this.Name = "AlertForm"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.AlertForm_FormClosing); + this.Load += new System.EventHandler(this.AlertForm_Load); + this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.AlertForm_KeyDown); + this.PopupMenu.ResumeLayout(false); + this.ResumeLayout(false); + + } + + /// + /// ボーダーメニュークリック時 + /// + /// + /// + private void BorderMenu_Click(object sender, EventArgs e) + { + using (var frm = new TextInputForm( + IniHelper.ReadValue(null, this.Name, INI_KEY_BORDER))) + { + if (frm.ShowDialog(this) == DialogResult.Cancel) return; + IniHelper.WriteValue(null, this.Name, INI_KEY_BORDER, frm.EditedText); + } + } + + private void All_MouseDown(object sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) return; + + this.Close(); + } + + private void AlertForm_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Escape) + { + Close(); + } + } + + /// + /// タイトルメニュークリック時 + /// + /// + /// + private void TitleMenu_Click(object sender, EventArgs e) + { + using (var frm = new TextInputForm( + IniHelper.ReadValue(null, this.Name, INI_KEY_TITLE))) + { + if (frm.ShowDialog(this) == DialogResult.Cancel) return; + IniHelper.WriteValue(null, this.Name, INI_KEY_TITLE, frm.EditedText); + } + _title.Text = IniHelper.ReadValue(null, this.Name, INI_KEY_TITLE, "重大エラー検知"); + } + + /// + /// フォーム閉じるとき + /// + /// + /// + private void AlertForm_FormClosing(object sender, FormClosingEventArgs e) + { + if ( + MessageBox.Show( + this, "アラート画面を閉じますか?", + "確認", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button2) == DialogResult.No) + { + // Noのとき閉じない + e.Cancel = true; + return; + } + } + + /// + /// 所属メニュークリック時 + /// + /// + /// + private void ShozokuMenu_Click(object sender, EventArgs e) + { + using (var frm = new TextInputForm( + IniHelper.ReadValue(null, this.Name, INI_KEY_ATTRIBUTE))) + { + if (frm.ShowDialog(this) == DialogResult.Cancel) return; + IniHelper.WriteValue(null, this.Name, INI_KEY_ATTRIBUTE, frm.EditedText); + } + } + + /// + /// 強烈モードメニュークリック時 + /// + /// + /// + private void KyoretsuModeMenu_Click(object sender, EventArgs e) + { + IniHelper.WriteValue(null, this.Name, "KyoretsuMode", KyoretsuModeMenu.Checked ? "0" : "1"); + } + + /// + /// ポップアップメニュー開くとき + /// + /// + /// + private void PopupMenu_Opening(object sender, CancelEventArgs e) + { + KyoretsuModeMenu.Checked = IniHelper.ReadValue(null, this.Name, "KyoretsuMode") == "1"; + } + + //protected override CreateParams CreateParams + //{ + // get + // { + // var cp = base.CreateParams; + // cp.ExStyle |= 0x00000080; // WS_EX_TOPMOSTを強調(常に最前面) + // return cp; + // } + //} + } +} diff --git a/Alert/Gamen/AlertForm.resx b/Alert/Gamen/AlertForm.resx new file mode 100644 index 0000000..37ff906 --- /dev/null +++ b/Alert/Gamen/AlertForm.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/Alert/ToastUxer.cs b/Alert/ToastUxer.cs new file mode 100644 index 0000000..f1bf081 --- /dev/null +++ b/Alert/ToastUxer.cs @@ -0,0 +1,34 @@ +using Microsoft.Toolkit.Uwp.Notifications; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Alert +{ + public static class ToastUxer + { + public static void ShowAlertToast( + string title, + DateTime dateTime, + string message, + string attributionText = "") + { + // トースト通知の組み立て + new ToastContentBuilder() + // 見出し(太字) + .AddText(title) + // 本文 + .AddText($"[{dateTime:yyyy/MM/dd HH:mm:ss.fff}]\r\n{message}") + // 令和風の装飾(任意:画像などを付けることも可能) + .AddAttributionText(attributionText /*"異常検知システムより"*/) + + // 通知の優先度を「緊急」に設定(通知センターに残る) + .SetToastScenario(ToastScenario.IncomingCall) + + // 画面に表示! + .Show(); + } + } +} diff --git a/Alert/WindowFlasherUxer.cs b/Alert/WindowFlasherUxer.cs new file mode 100644 index 0000000..c43256b --- /dev/null +++ b/Alert/WindowFlasherUxer.cs @@ -0,0 +1,38 @@ +using System.Runtime.InteropServices; // これが必要! +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Alert +{ + // --- ここからコピー --- + [StructLayout(LayoutKind.Sequential)] + public struct FLASHWINFO + { + public uint cbSize; public IntPtr hwnd; public uint dwFlags; + public uint uCount; public uint dwTimeout; + } + + public static class WindowFlasherUxer + { + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FlashWindowEx(ref FLASHWINFO pwfi); + + public const uint FLASHW_ALL = 3; // タスクバーとウィンドウ両方 + public const uint FLASHW_TIMERNOFG = 12; // 最前面に来るまで点滅し続ける + + public static void Flash(IntPtr hWnd) + { + FLASHWINFO fInfo = new FLASHWINFO(); + fInfo.cbSize = Convert.ToUInt32(Marshal.SizeOf(fInfo)); + fInfo.hwnd = hWnd; + fInfo.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG; + fInfo.uCount = uint.MaxValue; // 無限に点滅 + fInfo.dwTimeout = 0; + FlashWindowEx(ref fInfo); + } + } +} diff --git a/AutoTest/Command/Alias/CommandAliasUxer.cs b/AutoTest/Command/Alias/CommandAliasUxer.cs new file mode 100644 index 0000000..c880db5 --- /dev/null +++ b/AutoTest/Command/Alias/CommandAliasUxer.cs @@ -0,0 +1,22 @@ +using KssSmaPlaLib.AutoTest.Command.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Command.Alias +{ + public class CommandAliasUxer + { + private static Dictionary _reverseMap = new Dictionary(); + + public static void SetAleiasDic(Dictionary dic) + { + _reverseMap = dic; + } + + public static bool TryTranslate(string userCommand, out CommandEnumUxer.Command command) + => _reverseMap.TryGetValue(userCommand, out command); + } +} diff --git a/AutoTest/Command/Alias/Ini/IniReaderUxer.cs b/AutoTest/Command/Alias/Ini/IniReaderUxer.cs new file mode 100644 index 0000000..7d8f9e4 --- /dev/null +++ b/AutoTest/Command/Alias/Ini/IniReaderUxer.cs @@ -0,0 +1,63 @@ +using KssSmaPlaLib.AutoTest.Command.Definition; +using KssSmaPlaLib.AutoTest.Command.Translator; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Command.Alias.Ini +{ + public static class IniReaderUxer + { + public static bool Try(string iniFilePath, out Dictionary dic, out string resultMessage) + { + dic = new Dictionary(); + resultMessage = string.Empty; + + try + { + if (!File.Exists(iniFilePath)) + { + resultMessage = "エイリアスファイルが存在しません。"; + return false; + } + + var lines = File.ReadAllLines(iniFilePath, System.Text.Encoding.GetEncoding("shift_jis")); + + // ここでDTOにパース(例:セクションごとに分ける、キーと値を読み込むなど) + // ↓仮の処理(実際はINIパーサーCLがあると別府) + foreach (var line in lines) + { + if (line.StartsWith("#") || + line.StartsWith(";") || + string.IsNullOrWhiteSpace(line)) + continue; + + var parts = line.Split('='); + if (parts.Length == 2) + { + var command = parts[0].Trim(); + var alias = parts[1].Trim(); + + if (!CommandTranslatorUxer.TryTranslate(command, out var commandEnum)) + { + resultMessage = $"エイリアスファイルでコマンド名が不正です。[コマンド名:{command}]"; + return false; + } + + // エイリアス辞書登録 + dic[alias] = commandEnum; + } + } + return true; + } + catch (Exception ex) + { + resultMessage = $"エイリアスロードに失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + } +} diff --git a/AutoTest/Command/Alias/Loader/CommandAliasLoaderUxer.cs b/AutoTest/Command/Alias/Loader/CommandAliasLoaderUxer.cs new file mode 100644 index 0000000..23cf8bb --- /dev/null +++ b/AutoTest/Command/Alias/Loader/CommandAliasLoaderUxer.cs @@ -0,0 +1,27 @@ +using KssSmaPlaLib.AutoTest.Command.Alias.Ini; +using KssSmaPlaLib.AutoTest.Command.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Command.Alias.Loader +{ + public static class CommandAliasLoaderUxer + { + public const string COMMAND_ALIAS_FILE_PATH = ".\\command_alias.ini"; + + public static bool Try(out Dictionarydic, out string resultMessage) + { + resultMessage = string.Empty; + + if (!IniReaderUxer.Try(COMMAND_ALIAS_FILE_PATH, out dic, out var messageForMe)) + { + resultMessage = messageForMe; + return false; + } + return true; + } + } +} diff --git a/AutoTest/Command/Definition/CommandEnumUxer.cs b/AutoTest/Command/Definition/CommandEnumUxer.cs new file mode 100644 index 0000000..1769652 --- /dev/null +++ b/AutoTest/Command/Definition/CommandEnumUxer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Command.Definition +{ + public class CommandEnumUxer + { + /// + /// UIステップテストコマンド + /// + public enum Command + { + None, + Sleep, + SetText, + PushButton, + TryDbMoshimoshi + } + } +} diff --git a/AutoTest/Command/Definition/CommandParserUxer.cs b/AutoTest/Command/Definition/CommandParserUxer.cs new file mode 100644 index 0000000..f65b5aa --- /dev/null +++ b/AutoTest/Command/Definition/CommandParserUxer.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Command.Definition +{ + public static class CommandParserUxer + { + private static readonly Dictionary _map = new Dictionary() + { + { "Sleep", CommandEnumUxer.Command.Sleep }, + { "SetText", CommandEnumUxer.Command.SetText }, + { "PushButton", CommandEnumUxer.Command.PushButton }, + { "TryDbMoshimoshi", CommandEnumUxer.Command.TryDbMoshimoshi } + }; + + public static bool TryParse(string commandStr, out CommandEnumUxer.Command command) + => _map.TryGetValue(commandStr, out command); + } +} diff --git a/AutoTest/Command/Translator/CommandTranslatorUxer.cs b/AutoTest/Command/Translator/CommandTranslatorUxer.cs new file mode 100644 index 0000000..15ad947 --- /dev/null +++ b/AutoTest/Command/Translator/CommandTranslatorUxer.cs @@ -0,0 +1,23 @@ +using KssSmaPlaLib.AutoTest.Command.Definition; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Command.Translator +{ + public static class CommandTranslatorUxer + { + private static readonly Dictionary _reverseMap = new Dictionary() + { + { "寝る", CommandEnumUxer.Command.Sleep }, + { "あああをセット", CommandEnumUxer.Command.SetText }, + { "ボタン押す", CommandEnumUxer.Command.PushButton }, + { "もしもしDB", CommandEnumUxer.Command.TryDbMoshimoshi } + }; + + public static bool TryTranslate(string userCommand, out CommandEnumUxer.Command command) + => _reverseMap.TryGetValue(userCommand, out command); + } +} diff --git a/AutoTest/Core/StepTestRunnerUxer.cs b/AutoTest/Core/StepTestRunnerUxer.cs new file mode 100644 index 0000000..2ec9d11 --- /dev/null +++ b/AutoTest/Core/StepTestRunnerUxer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Core +{ + /// + /// 全体統括・ステップ順実行・ログ出力 + /// + public static class StepTestRunnerUxer + { + /// + /// 起動パラメータのUIテスト指定 + /// + private const string KIDOU_PARA_UITEST = "--uitest"; + + /// + /// メインPJのエントリーポイント用: + /// 起動パラよりUIテストモード判定 + /// + /// 起動パラメータ + /// UIテストモード? + public static bool IsModeByArgs(string[] args) + { + // 起動パラ判定 + return args.Any(m => m.ToLower() == KIDOU_PARA_UITEST); + } + } +} diff --git a/AutoTest/Core/TestModeInitializerUxer.cs b/AutoTest/Core/TestModeInitializerUxer.cs new file mode 100644 index 0000000..69d1a79 --- /dev/null +++ b/AutoTest/Core/TestModeInitializerUxer.cs @@ -0,0 +1,64 @@ +using DocumentFormat.OpenXml.Wordprocessing; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.AutoTest.Script; +using Serilog.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using KssSmaPlaLib.Batch.Log; +using KssSmaPlaLib.AutoTest.Command.Alias.Loader; +using KssSmaPlaLib.AutoTest.Command.Alias; +using KssSmaPlaLib.AutoTest.Script.Load; +using KssSmaPlaLib.AutoTest.Script.Storage; +using KssSmaPlaLib.AutoTest.Script.Parse; + +namespace KssSmaPlaLib.AutoTest.Core +{ + public static class TestModeInitializerUxer + { + public static bool TryInitialize(string[] args, out string resultMessage) + { + resultMessage = string.Empty; + + // UIステップテストモード判定 + var isMode = StepTestRunnerUxer.IsModeByArgs(args); + TestModeUxer.SetTestMode(isMode); + + if (!isMode) return true; + + // モードのとき、ロード(エイリアス) + if (!CommandAliasLoaderUxer.Try(out var aliasDic, out var messageForMe)) + { + resultMessage = messageForMe; + return false; + } + // 保存 + CommandAliasUxer.SetAleiasDic(aliasDic); + + // ロード(テストスクリプト) + if (!LoadTryUxer.TryLoad(out var stepInfoDTO, out messageForMe)) + { + resultMessage = messageForMe; + return false; + } + // 保存 + TestScriptStorageUxer.SetStepInfo(stepInfoDTO); + + // ステップ内容パース + foreach (var step in stepInfoDTO.StepList) + { + if (!ParseTryUxer.TryParse(step.Raw, out var parsed, out messageForMe)) + { + resultMessage = $"ステップ内容が不正です。[ステップNo:{step.StepNo},ステップテキスト:{step.Raw.StepText},エラー内容:{messageForMe}]"; + return false; + } + } + // ステップ情報ログ出力 + // ↓ToDo自動テスト用ログ出力を作る! + //StepLogUxer.StepInfoLog(stepInfoDTO); + return true; + } + } +} diff --git a/AutoTest/Core/TestModeUxer.cs b/AutoTest/Core/TestModeUxer.cs new file mode 100644 index 0000000..fd20776 --- /dev/null +++ b/AutoTest/Core/TestModeUxer.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Core +{ + public static class TestModeUxer + { + public static bool IsTestMode { get; private set; } + + public static void SetTestMode(bool value) + { + IsTestMode = value; + } + } +} diff --git a/AutoTest/Script/Load/LoadTryUxer.cs b/AutoTest/Script/Load/LoadTryUxer.cs new file mode 100644 index 0000000..de24f83 --- /dev/null +++ b/AutoTest/Script/Load/LoadTryUxer.cs @@ -0,0 +1,91 @@ +using KssSmaPlaLib.AutoTest.Script.Models; +using KssSmaPlaLib.AutoTest.Script.Parse; +using System; +using System.IO; +using System.Linq; + +namespace KssSmaPlaLib.AutoTest.Script.Load +{ + /// + /// UIテストスクリプトローダー + /// + public static class LoadTryUxer + { + /// + /// スクリプトファイルパス + /// + private const string UITEST_SCRIPT_FILE_PATH = "./test_script.ini"; + + /// + /// スクリプトロード + /// + /// [出力]テストスクリプトデータ + /// [出力]メッセージ + /// 成否 + public static bool TryLoad(out StepInfoDTO stepInfo, out string resultMessage) + { + stepInfo = new StepInfoDTO(); + resultMessage = string.Empty; + + try + { + if (!File.Exists(UITEST_SCRIPT_FILE_PATH)) + { + resultMessage = "スクリプトファイルが存在しません。"; + return false; + } + + var lines = File.ReadAllLines(UITEST_SCRIPT_FILE_PATH, System.Text.Encoding.GetEncoding("shift_jis")); + + stepInfo.Description = "なんか詳細...(笑)"; + stepInfo.InitialStepName = "なんか初期ステップ名...(笑)"; + + // ここでDTOにパース(例:セクションごとに分ける、キーと値を読み込むなど) + // ↓仮の処理(実際はINIパーサーCLがあると別府) + foreach (var line in lines) + { + if (line.StartsWith("#") || + line.StartsWith(";") || + string.IsNullOrWhiteSpace(line)) + continue; + + // 左辺と右辺分離 + var parts = line.Split(new[] { '=' }, 2); + + // 要素数違いは飛ばす + if (parts.Length != 2) continue; + + // 要素取得 + var no = parts[0].Trim(); + var text = parts[1].Trim(); + + // No重複チェック + if (stepInfo.StepList.Any(m => m.StepNo == no)) + { + resultMessage = $"ステップNoが重複しています。[ステップNo:{no}]"; + return false; + } + + // RawDTO + var raw = new StepDTO(no, text); + + // バリデーションと変換 + if (!ParseTryUxer.TryParse(raw, out var parsed, out string messageForMe)) + { + resultMessage = $"ステップエラーです。[行:{line},エラー内容:{messageForMe}"; + return false; + } + + // 正常:リストに追加 + stepInfo.StepList.Add(parsed); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"スクリプトロードに失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + } +} diff --git a/AutoTest/Script/Models/ParsedStepDTO.cs b/AutoTest/Script/Models/ParsedStepDTO.cs new file mode 100644 index 0000000..6537a5e --- /dev/null +++ b/AutoTest/Script/Models/ParsedStepDTO.cs @@ -0,0 +1,27 @@ +using KssSmaPlaLib.AutoTest.Command.Definition; +using KssSmaPlaLib.AutoTest.Script.Models; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Script.Models +{ + /// + /// パース済みのステップDTO(1ステップの情報) + /// + public class ParsedStepDTO + { + public string StepNo => Raw.StepNo; + public CommandEnumUxer.Command Command { get; set; } + public List Parameters { get; set; } + public StepDTO Raw { get; } + + public ParsedStepDTO(StepDTO raw) + { + Raw = raw; + } + } +} diff --git a/AutoTest/Script/Models/StepDTO.cs b/AutoTest/Script/Models/StepDTO.cs new file mode 100644 index 0000000..a7fc8f0 --- /dev/null +++ b/AutoTest/Script/Models/StepDTO.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Script.Models +{ + /// + /// ステップDTO(1ステップの情報) + /// + public class StepDTO + { + /// + /// ステップ番号 + /// + public string StepNo { get; set; } + + /// + /// ステップテキスト + /// + public string StepText { get; set; } + + public StepDTO(string stepNo, string stepText) + { this.StepNo = stepNo; this.StepText = stepText; } + } +} diff --git a/AutoTest/Script/Models/StepInfoDTO.cs b/AutoTest/Script/Models/StepInfoDTO.cs new file mode 100644 index 0000000..20c2245 --- /dev/null +++ b/AutoTest/Script/Models/StepInfoDTO.cs @@ -0,0 +1,19 @@ +using KssSmaPlaLib.AutoTest.Script; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Script.Models +{ + /// + /// UIテストステップ全体情報 + /// + public class StepInfoDTO + { + public List StepList { get; set; } = new List(); + public string InitialStepName { get; set; } // 最初に実行するステップ名 + public string Description { get; set; } // ステップ群の説明(任意) + } +} diff --git a/AutoTest/Script/Parse/Function/FunctionTryUxer.cs b/AutoTest/Script/Parse/Function/FunctionTryUxer.cs new file mode 100644 index 0000000..b0cde14 --- /dev/null +++ b/AutoTest/Script/Parse/Function/FunctionTryUxer.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.AutoTest.Script.Parse.Function +{ + public class FunctionTryUxer + { + public static bool TryFunction(string input, out string functionName, out List funcArgList,out string resultMessage) + { + functionName = ""; + funcArgList = new List(); + resultMessage = string.Empty; + + var match = Regex.Match(input, @"^(\w+)\((.*?)\)$"); + if (match.Success) + { + functionName = match.Groups[1].Value; + var args = match.Groups[2].Value; + if (!string.IsNullOrWhiteSpace(args)) + { + funcArgList.AddRange(args.Split(',')); + } + return true; + } + resultMessage = $"シグネチャ形式が不正です。[シグネチャ:{input}]"; + return false; + } + } +} diff --git a/AutoTest/Script/Parse/ParseTryUxer.cs b/AutoTest/Script/Parse/ParseTryUxer.cs new file mode 100644 index 0000000..c6ef885 --- /dev/null +++ b/AutoTest/Script/Parse/ParseTryUxer.cs @@ -0,0 +1,47 @@ +using DocumentFormat.OpenXml.Math; +using KssSmaPlaLib.AutoTest.Command.Alias; +using KssSmaPlaLib.AutoTest.Script.Models; +using KssSmaPlaLib.AutoTest.Script.Parse.Function; +using System.Linq; +using System.Reflection; + +namespace KssSmaPlaLib.AutoTest.Script.Parse +{ + public static class ParseTryUxer + { + public static bool TryParse(StepDTO raw, out ParsedStepDTO parsed, out string resultMessage) + { + resultMessage = string.Empty; + parsed = new ParsedStepDTO(raw); // Raw注入 + + // ステップNo + if (string.IsNullOrEmpty(raw.StepNo)) + { + resultMessage = $"UIテストステップ:ステップ番号が指定されていません。"; + return false; + } + + // ステップ情報:シグネチャのバリデーション + if (!FunctionTryUxer.TryFunction(raw.StepText, out var functionName, out var argList, out var messageForMe)) + { + resultMessage = $"UIテストステップ:ステップ情報のシグネチャの変換に失敗しました。[エラー内容:{messageForMe}]"; + return false; + } + + // 関数(機能)名 + if (!CommandAliasUxer.TryTranslate(functionName, out var commandEnum)) + { + resultMessage = $"UIテストステップ:ステップ情報の関数(機能)名が未登録です。[関数(機能)名:{functionName}]"; + return false; + } + + // コマンド(enum値) + parsed.Command = commandEnum; + + // パラメータ + parsed.Parameters = argList; + + return true; + } + } +} diff --git a/AutoTest/Script/Storage/TestScriptStorageUxer.cs b/AutoTest/Script/Storage/TestScriptStorageUxer.cs new file mode 100644 index 0000000..7a5a08b --- /dev/null +++ b/AutoTest/Script/Storage/TestScriptStorageUxer.cs @@ -0,0 +1,16 @@ +using KssSmaPlaLib.AutoTest.Script.Models; + +namespace KssSmaPlaLib.AutoTest.Script.Storage +{ + public static class TestScriptStorageUxer + { + private static StepInfoDTO _stepInfo = null; + + public static StepInfoDTO StepInfo { get { return _stepInfo; } } + + public static void SetStepInfo(StepInfoDTO stepInfo) + { + _stepInfo = stepInfo; + } + } +} diff --git a/Batch/Cleanup/CleanupStepHandlerUxer.cs b/Batch/Cleanup/CleanupStepHandlerUxer.cs new file mode 100644 index 0000000..8d4ee23 --- /dev/null +++ b/Batch/Cleanup/CleanupStepHandlerUxer.cs @@ -0,0 +1,51 @@ +using KssSmaPlaLib.Batch.Interface; +using KssSmaPlaLib.Cleanup; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static KssSmaPlaLib.Batch.Sftp.SftpStepHandlerUxer; + +namespace KssSmaPlaLib.Batch.Cleanup +{ + public class CleanupStepHandlerUxer + { + public FuncCleanupByIniResultDto TryFuncCleanupByIni( + string iniFilePath, + string secsionName) + { + const string KEY_FOLDER_PATH = "FolderPath"; + + var result = new FuncCleanupByIniResultDto(); + + var cuDic = IniReferenceResolveUxer.ReadSectionWithReference(iniFilePath, secsionName); + var folderPath = DictionaryUxer.GetValueOrDefault(cuDic, KEY_FOLDER_PATH, string.Empty); + if (string.IsNullOrEmpty(folderPath)) + { + LoggerUxer.Info($"フォルダパスが未指定のため、クリーンアップ機能をスキップしました。[キー:{KEY_FOLDER_PATH}]"); + result.Succeeded = true; + return result; + } + + if (!FileCleanupUxer.TryFileCleanupByDic(cuDic, out var messageForMe)) + { + result.ResultMessage = $"ファイルのクリーンナップに失敗しました。[エラー内容:{messageForMe}]"; + result.ExitCode = (int)RetCodeUxer.RetCode.CleanupError; + result.Succeeded = false; + return result; + } + result.Succeeded = true; + return result; + } + + public class FuncCleanupByIniResultDto : IFuncResultDto + { + public bool Succeeded { get; set; } = false; + public int ExitCode { get; set; } = 0; + public string ResultMessage { get; set; } = string.Empty; + } + } +} diff --git a/Batch/Commons/CommonUxer.cs b/Batch/Commons/CommonUxer.cs new file mode 100644 index 0000000..7a31069 --- /dev/null +++ b/Batch/Commons/CommonUxer.cs @@ -0,0 +1,96 @@ +using KssSmaPlaLib.Commons; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Batch.Commons +{ + public static class CommonUxer + { + public const string DIC_KEY_TITLE = "Title"; + public const string DIC_KEY_DAYS_TO_START_OFFSET = "DaysToStartOffset"; + public const string DIC_KEY_DAYS_TO_END_OFFSET = "DaysToEndOffset"; + + public static bool TryGetStartDate(Dictionary iniDic, out DateTime startDate, out string resultMessage) + { + startDate = DateTime.Today; + resultMessage = string.Empty; + + // 開始日 + DateTime? fromDate = null; + if (VariableUxer.Args.Count >= 2) + { + if (DateTime.TryParseExact( + VariableUxer.Args[1].Substring(1), + "yyyy/MM/dd", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var d) == false) + { + resultMessage = $"起動時パラメータの開始日(第2引数)が不正です。[起動時パラメータ:{string.Join(",", VariableUxer.Args)}]"; + return false; + } + fromDate = d; + } + + // 起動時パラメータで開始日指定あり + if (fromDate != null) + { + startDate = (DateTime)fromDate; + return true; + } + + // 起動時パラメータで開始日指定なし + + // オフセット日数取得 + var daysToStartOffset = DictionaryUxer.GetValueOrDefault(iniDic, DIC_KEY_DAYS_TO_START_OFFSET, "0"); + + if (int.TryParse(daysToStartOffset, out var days) == false) + { + // 加算日数エラー + resultMessage = $"設定ファイルの開始日オフセット(加算日数)の設定値が不正です。[{daysToStartOffset}]"; + return false; + } + // システム日付(本日日)に加算日数を加算して開始日とする + startDate = DateTime.Today.AddDays(days); + return true; + } + + /// + /// 終了日のオフセット指定があるときだけ、終了日を取得する + /// + /// + /// + /// + /// + /// + public static bool TryGetEndDate(Dictionary iniDic, DateTime startDate, out DateTime? endDate, out string resultMessage) + { + endDate = null; + resultMessage = string.Empty; + + // オフセット日数取得 + var daysToEndOffset = DictionaryUxer.GetValueOrDefault(iniDic, DIC_KEY_DAYS_TO_END_OFFSET, string.Empty); + + if (string.IsNullOrEmpty(daysToEndOffset)) + { + // オフセット日数なしのとき、終了日なし + return true; + } + + if (int.TryParse(daysToEndOffset, out var days) == false) + { + // 加算日数エラー + resultMessage = $"設定ファイルの終了日オフセット(加算日数)の設定値が不正です。[{daysToEndOffset}]"; + return false; + } + + // 開始日に加算日数を加算して終了日とする + endDate = startDate.AddDays(days); + return true; + } + } +} diff --git a/Batch/DTO/StepDto.cs b/Batch/DTO/StepDto.cs new file mode 100644 index 0000000..8ae2754 --- /dev/null +++ b/Batch/DTO/StepDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Batch.DTO +{ + public class StepDto + { + public string StepNo { get; set; } = string.Empty; + public string StepSignature { get; set; } = string.Empty; + } +} diff --git a/Batch/DTO/StepInfoDto.cs b/Batch/DTO/StepInfoDto.cs new file mode 100644 index 0000000..22af7b8 --- /dev/null +++ b/Batch/DTO/StepInfoDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Batch.DTO +{ + public class StepInfoDto + { + public string Title { get; set; } = string.Empty; + public List StepList { get; set; } = new List(); + } +} diff --git a/Batch/Dispatch/StepDispatcherUxer.cs b/Batch/Dispatch/StepDispatcherUxer.cs new file mode 100644 index 0000000..6135851 --- /dev/null +++ b/Batch/Dispatch/StepDispatcherUxer.cs @@ -0,0 +1,56 @@ +using KssSmaPlaLib.AutoTest.Script; +using Serilog.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using KssSmaPlaLib.Batch.Cleanup; +using KssSmaPlaLib.Batch.Sftp; +using KssSmaPlaLib.Batch.Interface; +using KssSmaPlaLib.Batch.Backup; +using KssSmaPlaLib.Batch.Exchange; +using KssSmaPlaLib.Batch.IO; + +namespace KssSmaPlaLib.Batch.Dispatch +{ + public class StepDispatcherUxer + { + // 利用可能ステップハンドラー + private readonly Dictionary> _handlers; + + public StepDispatcherUxer() + { + _handlers = new Dictionary>() + { + { "Cleanup", (iniPath, section) => new CleanupStepHandlerUxer().TryFuncCleanupByIni(iniPath,section) }, + { "SFTPUpload", (iniPath, section) => new SftpStepHandlerUxer().TryFuncSftpUploadByIni(iniPath,section) }, + { "SFTPDownload", (iniPath, section) => new SftpStepHandlerUxer().TryFuncSftpDownloadByIni(iniPath,section) }, + { "SFTPDelete", (iniPath, section) => new SftpStepHandlerUxer().TryFuncSftpDeleteByIni(iniPath,section) }, + { "Backup", (iniPath, section) => new BackupStepHandlerUxer().TryFuncBackupByIni(iniPath,section) }, + { "CsvToDb", (iniPath, section) => new CsvToDbStepHandlerUxer().TryFuncCsvToDbByIni(iniPath,section) }, + { "Delete", (iniPath, section) => new FileStepHandlerUxer().TryFuncFileDeleteByIni(iniPath,section) }, + }; + } + + public bool TryDispatch(string stepName, string iniPath, string sectionName, out string resultMessage, out IFuncResultDto funcResultDto) + { + resultMessage = string.Empty; + funcResultDto = null; + if (_handlers.TryGetValue(stepName, out var func)) + { + funcResultDto = func(iniPath,sectionName); + resultMessage = funcResultDto.ResultMessage; + return funcResultDto.Succeeded; + } + resultMessage = $"機能名が登録されていません。[機能名:{stepName}]"; + return false; + } + } + public class TryDispatchResultDto : IFuncResultDto + { + public string ResultMessage { get; set; } = string.Empty; + public int ExitCode { get; set; } = 0; + public bool Succeeded { get; set; } = false; + } +} diff --git a/Batch/Exchange/CsvToDbStepHandlerUxer.cs b/Batch/Exchange/CsvToDbStepHandlerUxer.cs new file mode 100644 index 0000000..f36ebe3 --- /dev/null +++ b/Batch/Exchange/CsvToDbStepHandlerUxer.cs @@ -0,0 +1,357 @@ +using KssSmaPlaLib.Batch.Commons; +using KssSmaPlaLib.Batch.Interface; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Exchange; +using KssSmaPlaLib.Ini; +using KssSmaPlaLib.IO.Database; +using KssSmaPlaLib.IO.File; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Batch.Exchange +{ + public class CsvToDbStepHandlerUxer + { + public FuncCsvToDbByIniResultDto TryFuncCsvToDbByIni( + string iniFilePath, + string sectionName) + { + const string KEY_SQL_FOLDER_PATH = "SqlFolderPath"; + const string KEY_CSV_FILE_NAME_PATTERNS = "CsvFileNamePatterns"; + const string KEY_INSERT_SQL_FILES = "InsertSqlFiles"; + const string KEY_DUP_CHECK_SQL_FILES = "DupCheckSqlFiles"; + const string KEY_DELETE_BEFORE_INSERT_SQL_FILES = "DeleteBeforeInsertSqlFiles"; + const string KEY_CSV_ENCODING = "CsvEncoding"; + const string KEY_DB_PROVIDER_NAME = "DbProviderName"; + const string KEY_DB_CONNECTION_STRING = "DbConnectionString"; + const string KEY_CSV_FOLDER_PATH = "CsvFolderPath"; + + var result = new FuncCsvToDbByIniResultDto(); + + var iniDic = IniReferenceResolveUxer.ReadSectionWithReference(iniFilePath, sectionName); + + // 開始日 + if (!CommonUxer.TryGetStartDate(iniDic, out var startDate, out var messageForMe)) + { + var msg = $"開始日の取得に失敗しました。[エラー内容:{messageForMe}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.StartDateError; + result.ResultMessage = msg; + return result; + } + + // DB接続確認 + if (DbUxer.TestConnectByDic(iniDic, out messageForMe) == false) + { + var msg = $"DB接続確認で失敗しました。[エラー内容:{messageForMe}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.DbConnectError; + result.ResultMessage = msg; + return result; + } + LoggerUxer.Info($"DB接続確認が成功しました。[プロバイダ名:{DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_PROVIDER_NAME, string.Empty)},接続文字列:{DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_CONNECTION_STRING, string.Empty)}]"); + if (!string.IsNullOrEmpty(messageForMe)) + { + // Infoメッセージがあれば出力 + LoggerUxer.Info(messageForMe); + } + + // SQLフォルダパス + var sqlFolderPath = DictionaryUxer.GetValueOrDefault(iniDic, KEY_SQL_FOLDER_PATH, string.Empty); + if (string.IsNullOrEmpty(sqlFolderPath)) + { + var msg = $"設定ファイルにSQLフォルダパスが設定されていません。[キー:{KEY_SQL_FOLDER_PATH}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SqlFolderPathNoSetting; + result.ResultMessage = msg; + return result; + } + + // SQLのロード + if (!DbUxer.TryLoadSqlFromText(sqlFolderPath, out messageForMe)) + { + var msg = $"SQLのロードに失敗しました。[フォルダパス:SQL,エラー内容:{messageForMe}"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SqlFileLoadError; + result.ResultMessage = msg; + return result; + } + LoggerUxer.Info(messageForMe); // 正常メッセージ + + // CSVファイル名パターン群 + var csvPatterns = DictionaryUxer.GetValueOrDefault(iniDic, KEY_CSV_FILE_NAME_PATTERNS, string.Empty); + if (string.IsNullOrEmpty(csvPatterns)) + { + var msg = $"設定ファイルのCSVファイル名パターンが設定されていません。[キー:{KEY_CSV_FILE_NAME_PATTERNS}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.CsvFileNamePatternsNotSetting; + result.ResultMessage = msg; + return result; + } + var csvPatternArray = csvPatterns.Split(','); + + // INSERT文SQLファイル群 + var insertSqlFiles = DictionaryUxer.GetValueOrDefault(iniDic, KEY_INSERT_SQL_FILES, string.Empty); + var insertSqlFilesArray = insertSqlFiles.Split(','); + + // 重複チェックSELECT文SQLファイル群 + var dupCheckSqlFiles = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DUP_CHECK_SQL_FILES, string.Empty); + var dupCheckSqlFilesArray = dupCheckSqlFiles.Split(','); + + // 挿入前の削除SQLファイル群 + var deleteBeforeInsertSqlFiles = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DELETE_BEFORE_INSERT_SQL_FILES, string.Empty); + var deleteBeforeInsertSqlFilesArray = deleteBeforeInsertSqlFiles.Split(','); + + // 要素数チェック + if (csvPatternArray.Length != insertSqlFilesArray.Length || + csvPatternArray.Length != dupCheckSqlFilesArray.Length || + csvPatternArray.Length != deleteBeforeInsertSqlFilesArray.Length) + { + var msg = $"設定ファイルの要素数が異なります。[{KEY_CSV_FILE_NAME_PATTERNS},{KEY_INSERT_SQL_FILES},{KEY_DUP_CHECK_SQL_FILES},{KEY_DELETE_BEFORE_INSERT_SQL_FILES}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.IniFileItemCountError; + result.ResultMessage = msg; + return result; + } + LoggerUxer.Info($"設定ファイルの要素数が一致しています。[要素数:{csvPatternArray.Length},キー:{KEY_CSV_FILE_NAME_PATTERNS},{KEY_INSERT_SQL_FILES},{KEY_DUP_CHECK_SQL_FILES},{KEY_DELETE_BEFORE_INSERT_SQL_FILES}]"); + + var encodingTypeName = DictionaryUxer.GetValueOrDefault(iniDic, KEY_CSV_ENCODING, "utf8"); + var encodingType = EncodingUxer.GetEncodingType(encodingTypeName); + if (encodingType == null) + { + encodingType = EncodingUxer.EncodingType.Utf8WithoutBom; + LoggerUxer.Warn($"CSVエンコーディングタイプ名が不正です。UFT-8で処理します。[キー:{KEY_CSV_ENCODING},値:{encodingTypeName}]"); + } + + try + { + // DB接続 + if (!DbUxer.TryCreateByDic(iniDic, out DbUxer con, out messageForMe, false/* ★★★ トランザクション有無:true */)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.DbConnectError; + result.ResultMessage = messageForMe; + return result; + } + LoggerUxer.Info($"DBに接続しました。[プロバイダ名:{DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_PROVIDER_NAME, string.Empty)},接続文字列:{DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_CONNECTION_STRING, string.Empty)}]"); + + var csvFolderPath = DictionaryUxer.GetValueOrDefault(iniDic, KEY_CSV_FOLDER_PATH, "."); + LoggerUxer.Info($"[CSVフォルダパス:{csvFolderPath}]"); + + using (con) + { + // CSVパターン毎 + for (var cnt = 0; cnt < csvPatternArray.Length; cnt++) + { + LoggerUxer.Info($"[CSVパターン{cnt + 1}:{csvPatternArray[cnt]}]"); + + var insertSqlFile = insertSqlFilesArray[cnt]; + var csvFileNamePattern = csvPatternArray[cnt]; + var dupCheckSqlFile = dupCheckSqlFilesArray[cnt]; + var deleteBeforeInsertSqlFile = deleteBeforeInsertSqlFilesArray[cnt]; + + LoggerUxer.Info($"[該当ファイル数:{Directory.GetFiles(csvFolderPath, csvFileNamePattern).Length}]"); + + // ファイル毎 + foreach (var path in Directory.GetFiles(csvFolderPath, csvFileNamePattern)) + { + if (deleteBeforeInsertSqlFile == "DeleteBeforeInsertWeOrder") + { + // 溶接専用!🔥🌊 + + // 最小日の取得(溶接専用ロジック) + var minDic = new Dictionary(); + // CSV読み込み+最小値取得 + if (!GetMinValueByKeyFromCsvFiles(path, "line_id", "work_date", ref minDic, out var messageForMe0)) + { + LoggerUxer.Error(messageForMe0); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.InsertDbFromCsvFilesError; + result.ResultMessage = messageForMe; + return result; + } + LoggerUxer.Info($"ライン毎の最小日の取得を完了"); + + if (!string.IsNullOrEmpty(deleteBeforeInsertSqlFile)) + { + // 挿入前の削除SQLを実行 + LoggerUxer.Info($"挿入前の削除SQLを実行[SQLファイル:{deleteBeforeInsertSqlFile}]"); + + var totalResultCount = 0; + foreach (var kv in minDic) + { + var resultCount = 0; + if (!con.ExecuteNonQuery( + deleteBeforeInsertSqlFile, + new Dictionary() + { + { "LINE_ID", kv.Key }, + { "START_DATE", kv.Value } + }, + out resultCount, + out messageForMe)) + { + var msg = $"挿入前の削除が失敗しました。[エラー内容:{messageForMe}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.DeleteBeforeInsertException; + result.ResultMessage = msg; + return result; + } + totalResultCount += resultCount; + LoggerUxer.Info($"挿入前の削除成功[ラインID:{kv.Key},開始日:{kv.Value},削除件数:{resultCount}]"); + } + LoggerUxer.Info($"挿入前の削除SQLを完了[更新件数:{totalResultCount}]"); + } + else + { + LoggerUxer.Info($"挿入前の削除処理はSQLが未指定のためスキップしました。[キー:{KEY_DELETE_BEFORE_INSERT_SQL_FILES}]"); + } + } + else + { + if (!string.IsNullOrEmpty(deleteBeforeInsertSqlFile)) + { + // 挿入前の削除SQLを実行 + LoggerUxer.Info($"挿入前の削除SQLを実行[SQLファイル:{deleteBeforeInsertSqlFile}]"); + + var totalResultCount = 0; + if (!con.ExecuteNonQuery( + deleteBeforeInsertSqlFile, + new Dictionary() + { + { "START_DATE", startDate } + }, + out var resultCount, + out messageForMe)) + { + var msg = $"挿入前の削除が失敗しました。[エラー内容:{messageForMe}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.DeleteBeforeInsertException; + result.ResultMessage = msg; + return result; + } + totalResultCount += resultCount; + LoggerUxer.Info($"挿入前の削除SQLを完了[更新件数:{totalResultCount}]"); + } + else + { + LoggerUxer.Info($"挿入前の削除処理はSQLが未指定のためスキップしました。[キー:{KEY_DELETE_BEFORE_INSERT_SQL_FILES}]"); + } + } + + // CSV読み込み+DB挿入 + LoggerUxer.Info($"CSV読み込み+DB挿入を実行[ファイルパス:{path}]"); + if (!CsvDbSyncUxer.InsertDbFromCsvFiles(iniDic, con, path, dupCheckSqlFile, insertSqlFile, out messageForMe, (EncodingUxer.EncodingType)encodingType)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.InsertDbFromCsvFilesError; + result.ResultMessage= messageForMe; + return result; + } + LoggerUxer.Info($"CSV読み込み+DB挿入を完了"); + } + } + // コミット + con.Commit(); + } + LoggerUxer.Info($"DB処理が完了し、DB接続を終了しました。"); + } + catch (Exception ex) + { + var msg = $"DB処理にてエラーが発生しました。[エラー内容:{ex.Message}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.CsvToDbException; + result.ResultMessage = msg; + return result; + } + result.Succeeded = true; + return result; + } + + /// + /// CSVよりキー毎に最小値を取得し辞書で返す + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool GetMinValueByKeyFromCsvFiles( + string csvFilePath, + string keyColName, + string valueColName, + ref Dictionary minDic, + out string resultMessage, + EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom) + where TKeyType : IComparable + where TValueType : IComparable + { + // 👇参照型(追記とする)ゆえやめた + //minDic = new Dictionary(); + resultMessage = string.Empty; + + try + { + // CSV読み込み + if (!CsvUxer.ImportFromCsv(csvFilePath, out var rs, out var messageForMe, encodingType)) + { + resultMessage = messageForMe; + return false; + } + + //LoggerUxer.Info($"[CSV件数:{rs.Count}]"); + + foreach (var rec in rs) + { + TKeyType key; + TValueType val; + try { key = (TKeyType)Convert.ChangeType(rec[keyColName], typeof(TKeyType)); } + catch (Exception) { continue; }; + try { val = (TValueType)Convert.ChangeType(rec[valueColName], typeof(TValueType)); } + catch (Exception) { continue; } + + if (minDic.ContainsKey(key)) + { + minDic[key] = Comparer.Default.Compare(val, minDic[key]) < 0 ? val : minDic[key]; + } + else + { + minDic[key] = val; + } + } + return true; + } + catch (Exception ex) + { + resultMessage = $"最小日時取得時にエラーが発生しました。[エラー内容:{ex.Message}]"; + return false; + } + } + } + public class FuncCsvToDbByIniResultDto : IFuncResultDto + { + public bool Succeeded { get; set; } = false; + public int ExitCode { get; set; } = 0; + public string ResultMessage { get; set; } = string.Empty; + } +} diff --git a/Batch/IO/FileStepHandlerUxer.cs b/Batch/IO/FileStepHandlerUxer.cs new file mode 100644 index 0000000..a5022a0 --- /dev/null +++ b/Batch/IO/FileStepHandlerUxer.cs @@ -0,0 +1,87 @@ +using KssSmaPlaLib.Batch.Interface; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Ini; +using KssSmaPlaLib.IO.File; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Net.WebRequestMethods; + +namespace KssSmaPlaLib.Batch.IO +{ + public class FileStepHandlerUxer + { + public FuncFileDeleteByIniResultDto TryFuncFileDeleteByIni( + string iniFilePath, + string sectionName) + { + const string KEY_FOLDER_PATH = "FolderPath"; + const string KEY_FILE_NAME_PATTERNS = "FileNamePatterns"; + + FuncFileDeleteByIniResultDto result = new FuncFileDeleteByIniResultDto(); + + var iniDic = IniReferenceResolveUxer.ReadSectionWithReference(iniFilePath, sectionName); + + var folderPath = DictionaryUxer.GetValueOrDefault(iniDic, KEY_FOLDER_PATH, string.Empty); + if (string.IsNullOrEmpty(folderPath)) + { + var msg = $"設定ファイルのフォルダパスが指定されていません。[キー:{KEY_FOLDER_PATH}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.FileDeleteFolderPathNotSetting; + result.ResultMessage = msg; + return result; + } + if (!Directory.Exists(folderPath)) + { + var msg = $"設定ファイルのフォルダパスが存在しません。[キー:{KEY_FOLDER_PATH},値:{folderPath}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.FileDeleteFolderPathNotExist; + result.ResultMessage = msg; + return result; + } + + var fileNamePatterns = DictionaryUxer.GetValueOrDefault(iniDic, KEY_FILE_NAME_PATTERNS, string.Empty); + if (string.IsNullOrEmpty(fileNamePatterns)) + { + var msg = $"設定ファイルのファイル名パターンが指定されていません。[キー:{KEY_FILE_NAME_PATTERNS}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.FileDeleteFileNamePatternNotSetting; + result.ResultMessage = msg; + return result; + } + var fileNamePatternsArray = fileNamePatterns.Split(','); + + // ローカルのCSVファイルの削除 + LoggerUxer.Info($"ファイルの削除を実行"); + for (var cnt = 0; cnt < fileNamePatternsArray.Length; cnt++) + { + var fileNamePattern = fileNamePatternsArray[cnt]; + LoggerUxer.Info($"[ファイル名パターン:{fileNamePattern}]"); + if (!FileUxer.TryDeletePatternFiles(folderPath, fileNamePattern, out var fileCount, out var messageForMe)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.FileDeletePatternFilesError; + result.ResultMessage = messageForMe; + return result; + } + LoggerUxer.Info($"[削除件数:{fileCount}]"); + } + LoggerUxer.Info($"ローカルのCSVファイルの削除を完了"); + result.Succeeded = true; + return result; + } + } + public class FuncFileDeleteByIniResultDto : IFuncResultDto + { + public bool Succeeded { get; set; } = false; + public int ExitCode { get; set; } = 0; + public string ResultMessage { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Batch/Interface/IFuncResultDto.cs b/Batch/Interface/IFuncResultDto.cs new file mode 100644 index 0000000..5c66524 --- /dev/null +++ b/Batch/Interface/IFuncResultDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Batch.Interface +{ + public interface IFuncResultDto + { + bool Succeeded { get; set; } + int ExitCode { get; set; } + string ResultMessage { get; set; } + } +} diff --git a/Batch/Sftp/SftpStepHandlerUxer.cs b/Batch/Sftp/SftpStepHandlerUxer.cs new file mode 100644 index 0000000..a68e13c --- /dev/null +++ b/Batch/Sftp/SftpStepHandlerUxer.cs @@ -0,0 +1,246 @@ +using KssSmaPlaLib.Batch.Interface; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Communications; +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Interop; +using static System.Net.WebRequestMethods; + +namespace KssSmaPlaLib.Batch.Sftp +{ + public class SftpStepHandlerUxer + { + public FuncSftpByIniResultDto TryFuncSftpUploadByIni( + string iniFilePath, + string sessionName) + { + const string KEY_HOST = "Host"; + const string KEY_REMOTE_PATH = "RemotePath"; + const string KEY_FILE_NAME_PATTERNS = "FileNamePatterns"; + const string KEY_IN_FOLDER_PATH = "InFolderPath"; + + var result = new FuncSftpByIniResultDto(); + + var iniSftpDic = IniReferenceResolveUxer.ReadSectionWithReference(iniFilePath, sessionName); + var host = DictionaryUxer.GetValueOrDefault(iniSftpDic, KEY_HOST, string.Empty); + if (string.IsNullOrEmpty(host)) + { + LoggerUxer.Info("ホストが未指定のためSFTPアップロード機能をスキップしました。"); + result.Succeeded = true; + return result; + } + + // オブジェクト生成 + if (!SftpUxer.TryCreateByDic(iniSftpDic, out SftpUxer sftp, out var messageForMe)) + { + result.ResultMessage = $"SFTPアップロード機能でオブジェクト生成に失敗しました。[エラー内容:{messageForMe}]"; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpObjectCreateError; + result.Succeeded = false; + return result; + } + + // テスト接続 + if (!sftp.TestConnect(out messageForMe)) + { + result.ResultMessage = $"SFTPアップロード機能でテスト接続に失敗しました。[エラー内容:{messageForMe}]"; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpTestConnectError; + result.Succeeded = false; + return result; + } + LoggerUxer.Info($"SFTPテスト接続成功"); + + // リモートパス + var remotePath = DictionaryUxer.GetValueOrDefault(iniSftpDic, KEY_REMOTE_PATH, string.Empty); + + // 入力フォルダパス + var inFolderPath = DictionaryUxer.GetValueOrDefault(iniSftpDic, KEY_IN_FOLDER_PATH, "."); + if (!Directory.Exists(inFolderPath)) + { + result.ResultMessage = $"SFTPアップロード機能で入力フォルダパスが存在しません。[{KEY_IN_FOLDER_PATH}:{inFolderPath}]"; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpInFolderPathNotExist; + result.Succeeded = false; + return result; + } + + // ファイル名パターン + var fileNamePatterns = DictionaryUxer.GetValueOrDefault(iniSftpDic, KEY_FILE_NAME_PATTERNS, "*"); + var fileNamePatternList = fileNamePatterns.Split(',').ToList(); + + // ファイルパスリストの作成 + var filePathList = new List(); + foreach(var pattern in fileNamePatternList) + { + foreach(var filePath in Directory.GetFiles(inFolderPath, pattern)) + { + filePathList.Add(filePath); + } + } + LoggerUxer.Info($"[対象ファイル数:{filePathList.Count},対象ファイルパス:{string.Join(",", filePathList)}]"); + + if (filePathList.Count == 0) + { + var msg = $"SFTPアップロード機能で対象ファイルが無かったためSTFPアップロードは実行されませんでした。"; + LoggerUxer.Info(msg); + result.ResultMessage = msg; + result.Succeeded = true; + return result; + } + + // 対象のアップロード + if (!sftp.Upload(filePathList, remotePath, out messageForMe)) + { + result.ResultMessage = $"SFTPアップロード機能のアップロードに失敗しました。[エラー内容:{messageForMe}]"; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpUploadError; + result.Succeeded = false; + return result; + } + LoggerUxer.Info($"SFTPアップロード機能完了[対象ファイル数:{filePathList.Count},対象ファイル:{string.Join(",", filePathList)},リモートパス:{remotePath},ファイル名パターン:{string.Join(",", fileNamePatternList)}]"); + result.Succeeded = true; + return result; + } + + public FuncSftpByIniResultDto TryFuncSftpDownloadByIni( + string iniFilePath, + string sectionName) + { + const string KEY_HOST = "Host"; + const string KEY_REMOTE_PATH = "RemotePath"; + const string KEY_OUT_FOLDER_PATH = "OutFolderPath"; + const string KEY_FILE_NAME_PATTERNS = "FileNamePatterns"; + + var result = new FuncSftpByIniResultDto(); + + var iniDic = IniReferenceResolveUxer.ReadSectionWithReference(iniFilePath, sectionName); + + var host = DictionaryUxer.GetValueOrDefault(iniDic, KEY_HOST, string.Empty); + if (!string.IsNullOrEmpty(host)) + { + LoggerUxer.Info($"SFTPダウンロード処理を開始します。"); + + try + { + // SFTPから対象ファイルをダウンロード + + // オブジェクト生成 + if (!SftpUxer.TryCreateByDic(iniDic, out SftpUxer sftp, out string messageForMe)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpObjectCreateError; + result.ResultMessage = messageForMe; + return result; + } + + // テスト接続 + if (!sftp.TestConnect(out messageForMe)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpTestConnectError; + result.ResultMessage = messageForMe; + return result; + } + LoggerUxer.Info($"SFTPテスト接続成功"); + + // パターンにマッチするファイルのダウンロード + var fileNamePatterns = DictionaryUxer.GetValueOrDefault(iniDic, KEY_FILE_NAME_PATTERNS, string.Empty); + if (string.IsNullOrEmpty(fileNamePatterns)) + { + var msg = $"SFTPのファイル名パターンが設定されていません。[キー:{KEY_FILE_NAME_PATTERNS}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpCsvFileNamePatternsNotSetting; + result.ResultMessage = msg; + return result; + } + + var remotePath = DictionaryUxer.GetValueOrDefault(iniDic, KEY_REMOTE_PATH, string.Empty); + var outFolderPath = DictionaryUxer.GetValueOrDefault(iniDic, KEY_OUT_FOLDER_PATH, string.Empty); + LoggerUxer.Info($"[リモートパス:{remotePath},CSV一時フォルダ:{outFolderPath}]"); + + foreach (var pattern in fileNamePatterns.Split(',')) + { + LoggerUxer.Info($"[ファイルパターン:{pattern}]"); + + if (!sftp.DownloadMatchingFiles(remotePath, outFolderPath, out var downloadedFileCount, out messageForMe, pattern)) + { + var msg = $"STFPでのダウンロードに失敗しました。[エラー内容:{messageForMe}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpMatchFilesDownloadError; + result.ResultMessage = msg; + return result; + } + + LoggerUxer.Info($"[ダウンロードしたファイル数:{downloadedFileCount}]"); + } + result.Succeeded = true; + return result; + } + catch (Exception ex) + { + var msg = $"SFTPダウンロードでエラーが発生しました。[エラー内容:{ex.Message}]"; + LoggerUxer.Error(msg); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpDownloadException; + result.ResultMessage = msg; + return result; + } + } + else + { + LoggerUxer.Info($"SFTP処理の設定がないため、スキップします。"); + result.Succeeded = true; + return result; + } + } + public FuncSftpByIniResultDto TryFuncSftpDeleteByIni( + string iniFilePath, + string sectionName) + { + const string KEY_REMOTE_PATH = "RemotePath"; + const string KEY_FILE_NAME_PATTERNS = "FileNamePatterns"; + + FuncSftpByIniResultDto result = new FuncSftpByIniResultDto(); + + var iniDic = IniReferenceResolveUxer.ReadSectionWithReference(iniFilePath, sectionName); + + // オブジェクト生成 + if (!SftpUxer.TryCreateByDic(iniDic, out SftpUxer sftp, out string messageForMe)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpObjectCreateError; + result.ResultMessage = messageForMe; + return result; + } + + foreach (var pattern in iniDic[KEY_FILE_NAME_PATTERNS].Split(',')) + { + if (!sftp.DeleteMatchingFiles(iniDic[KEY_REMOTE_PATH], out var deletedFileCount, out messageForMe, pattern)) + { + LoggerUxer.Error(messageForMe); + result.Succeeded = false; + result.ExitCode = (int)RetCodeUxer.RetCode.SftpDeleteMatchingFilesError; + result.ResultMessage = messageForMe; + return result; + } + LoggerUxer.Info($"SFTPのファイルを削除しました。[ファイル名パターン:{pattern},削除ファイル数:{deletedFileCount}]"); + } + result.Succeeded = true; + return result; + } + + public class FuncSftpByIniResultDto: IFuncResultDto + { + public bool Succeeded { get; set; } = false; + public int ExitCode { get; set; } = 0; + public string ResultMessage { get; set; } = string.Empty; + } + } +} diff --git a/Batch/StepInfo/StepInfoUxer.cs b/Batch/StepInfo/StepInfoUxer.cs new file mode 100644 index 0000000..67a5f44 --- /dev/null +++ b/Batch/StepInfo/StepInfoUxer.cs @@ -0,0 +1,54 @@ +using KssSmaPlaLib.Batch.DTO; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.AutoTest.Script; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using KssSmaPlaLib.Batch.Commons; + +namespace KssSmaPlaLib.Batch.StepInfo +{ + public static class StepInfoUxer + { + public static bool TryCreateStepInfoFromIniDic( + Dictionary iniDic, + out StepInfoDto stepInfo, + out string resultMessage) + { + stepInfo = new StepInfoDto + { + StepList = new List() + }; + resultMessage = string.Empty; + + foreach (var key in iniDic.Keys.OrderBy(m => m)) + { + // コメント行は除く + if (key.Trim().StartsWith(";")) continue; + + // タイトル + if (key.Trim() == CommonUxer.DIC_KEY_TITLE) + { + stepInfo.Title = iniDic[key]; + continue; + } + + stepInfo.StepList.Add(new StepDto() + { + StepNo = key.Trim(), + StepSignature = iniDic[key].Trim(), + }); + } + + if (stepInfo.StepList.Count == 0) + { + resultMessage = $"設定ファイルに処理区分のステップ情報が登録されていません。[タイトル:{stepInfo.Title}]"; + return false; + } + + return true; + } + } +} diff --git a/CL_設計別府条項.md b/CL_設計別府条項.md new file mode 100644 index 0000000..aee5ce6 --- /dev/null +++ b/CL_設計別府条項.md @@ -0,0 +1,29 @@ +# CL設計ガイドライン:別府条項(試作版) + +このドキュメントは、CL資産化における設計思想「別府条項」をまとめたものです。 +今後、気づきや発見があるたびに追記していく「Smapla DP(Design Pattern)」の母体となります。 + +--- + +## 🧠 条項一覧 + +### 1. クラス名はCL内で一意であるべき +- **理由**:インテリセンス選択ミスによる論理エラーの潜在化を防ぐため。 +- **背景**:同一名クラスが存在すると、シグネチャ一致による誤参照が発生しやすく、静的解析でも検出困難。 +- **別府度**:Smapla別府(高度PGX別府) + +### 2. メソッド名のCL内重複は歓迎(意味合いが確定している場合) +- **理由**:同名メソッドが意味合いで区別される設計はPGX的に直感的でわかりやすい。 +- **例**:`System.IO.File.Exists` と `System.IO.Directory.Exists` +- **別府度**:まそさん別府(PGX別府) + +--- + +## 🔥 今後の運用 +- 本ファイルはプロジェクトルートに配置し、随時追記可能とする。 +- 新たな思想・発見があれば、条項として追加し「Smapla DP」として昇華。 + +--- + +## 📝 編集履歴 +- 2025-10-16: 初版作成 by 小佐井 昇 & 昼コパ \ No newline at end of file diff --git a/Cleanup/FileCleanupUxer.cs b/Cleanup/FileCleanupUxer.cs new file mode 100644 index 0000000..8525adb --- /dev/null +++ b/Cleanup/FileCleanupUxer.cs @@ -0,0 +1,80 @@ +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.IO.File; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Cleanup +{ + public static class FileCleanupUxer + { + public static bool TryFileCleanupByDic( + Dictionary iniDic, + out string resultMessage) + { + const string KEY_FOLDER_PATH = "FolderPath"; + const string KEY_MAX_DAYS = "MaxDays"; + const string KEY_MAX_FILE_COUNT = "MaxFileCount"; + + var folderPath = DictionaryUxer.GetValueOrDefault(iniDic, KEY_FOLDER_PATH, string.Empty); + if (string.IsNullOrEmpty(folderPath)) + { + resultMessage = $"クリーンナップ処理用フォルダパスが設定されていません。[キー:{KEY_FOLDER_PATH}]"; + return false; + } + if (!Directory.Exists(folderPath)) + { + resultMessage = $"クリーンナップ処理用フォルダパスが存在しません。[キー:{KEY_FOLDER_PATH},値:{folderPath}]"; + return false; + } + + var str = DictionaryUxer.GetValueOrDefault(iniDic, KEY_MAX_DAYS, "99999"); + if (!int.TryParse(str, out var maxDays) || maxDays < 0) + { + resultMessage = $"クリーンナップ処理用最大保持日数が不正です。[キー:{KEY_MAX_DAYS},値:{str}]"; + return false; + } + LoggerUxer.Info($"[クリーンナップ対象フォルダパス:{folderPath}]"); + LoggerUxer.Info($"[クリーンナップ最大保持日数:{maxDays}]"); + + var now = DateTime.Now; + var thresholdDate = now.AddDays(-(maxDays - 1)).Date; + LoggerUxer.Info($"[クリーンナップ保持再過去日:{thresholdDate:yyyy/MM/dd}]"); + + str = DictionaryUxer.GetValueOrDefault(iniDic, KEY_MAX_FILE_COUNT, "99999"); + if (!int.TryParse(str, out var maxFileCount) || maxFileCount < 0) + { + resultMessage = $"クリーンナップ処理用最大保持ファイル数が不正です。[キー:{KEY_MAX_FILE_COUNT},値:{str}]"; + return false; + } + LoggerUxer.Info($"[クリーンナップ最大保持ファイル数:{maxFileCount}]"); + + var ret = TryFileCleanup(folderPath, thresholdDate, maxFileCount, out resultMessage); + LoggerUxer.Info($"クリーンナップ処理が終了しました。"); + return ret; + } + + public static bool TryFileCleanup(string folderPath, DateTime thresholdDate, int maxFileCount, out string resultMessage) + { + resultMessage = string.Empty; + + // フォルダの最大日数より過去のファイルを削除 + if (!FileUxer.TryApplyActionToFiles(folderPath, f => f.LastWriteTime < thresholdDate, f => f.Delete(), out var deleteFileCount, out var messageForMe, "日数超過削除")) + { + resultMessage = $"バックアップフォルダ過去ファイル削除(期間制限):{messageForMe}"; + return false; + } + + // バックアップフォルダの最大ファイル数超過時に古いファイルから削除 + if (!FileUxer.TryApplyActionToFiles(folderPath, f => true, f => f.Delete(), out deleteFileCount, out messageForMe, "ファイル数超過削除", true, null, maxFileCount)) + { + resultMessage = $"バックアップフォルダ過去ファイル削除(ファイル数制限):{messageForMe}"; + return false; + } + return true; + } + } +} diff --git a/Commons/Conversion/KanaConvertUxer.cs b/Commons/Conversion/KanaConvertUxer.cs new file mode 100644 index 0000000..58fbd9d --- /dev/null +++ b/Commons/Conversion/KanaConvertUxer.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons.Conversion +{ + public static class KanaConvertUxer + { + /// + /// 半角カタカナを全角カタカナに変換します。 + /// 空文字やnullはそのまま返却します。 + /// + public static bool TryConvertHankakuToZenkaku(string input, out string result, out string resultMessage) + { + result = input; + resultMessage = string.Empty; + + if (string.IsNullOrEmpty(input)) + return true; + + // 半角カタカナが含まれているかチェック + if (!Regex.IsMatch(input, @"[\uFF61-\uFF9F]")) + return true; + + // 変換処理 + result = Microsoft.VisualBasic.Strings.StrConv(input, Microsoft.VisualBasic.VbStrConv.Katakana | Microsoft.VisualBasic.VbStrConv.Wide, 0); + return true; + } + + /// + /// 全角カタカナを半角カタカナに変換します。 + /// 空文字やnullはそのまま返却します。 + /// + public static bool TryConvertZenkakuToHankaku(string input, out string result, out string resultMessage) + { + result = input; + resultMessage = string.Empty; + + if (string.IsNullOrEmpty(input)) + return true; + + // 全角カタカナが含まれているかチェック(ざっくり) + if (!Regex.IsMatch(input, @"[\u30A0-\u30FF]")) + return true; + + // 変換処理 + result = Microsoft.VisualBasic.Strings.StrConv(input, Microsoft.VisualBasic.VbStrConv.Katakana | Microsoft.VisualBasic.VbStrConv.Narrow, 0); + return true; + } + } +} diff --git a/Commons/DateHelperUxer.cs b/Commons/DateHelperUxer.cs new file mode 100644 index 0000000..974dfee --- /dev/null +++ b/Commons/DateHelperUxer.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public static class DateHelperUxer + { + + + /// + /// 指定した年と月の末日を返します。 + /// + /// 年(例: 2025) + /// 月(1~12) + /// 末日(28~31) + public static bool TryGetLastDayOfMonth(int year, int month, out int matsubi,out string resultMessage) + { + matsubi = -1; + resultMessage = string.Empty; + + if (month < 1 || month > 12) + { + resultMessage = "月が1~12の範囲ではありません。"; + return false; + } + + // DateTime構造体を使って末日を取得 + matsubi = DateTime.DaysInMonth(year, month); + return true; + } + } +} diff --git a/Commons/DictionaryUxer.cs b/Commons/DictionaryUxer.cs new file mode 100644 index 0000000..af42634 --- /dev/null +++ b/Commons/DictionaryUxer.cs @@ -0,0 +1,55 @@ +using KssSmaPlaLib.IO.File; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public static class DictionaryUxer + { + /// + /// 指定されたキーに対応する値(string)を取得します。キーが存在しない場合は 指定されたデフォルト値(未指定時はnull) を返します。 + /// + public static TValue GetValueOrDefault( + Dictionary dict, + TKey key, + TValue defaultValue = default) + { + return dict.TryGetValue(key, out TValue value) ? value : defaultValue; + } + + public static bool CreateNameToIdDic(string pcsv, out Dictionary dic, out string resultMessage) + { + resultMessage = string.Empty; + dic = new Dictionary(); + + if (string.IsNullOrEmpty(pcsv)) return true; + + var csvArray = pcsv.Split('|'); + foreach (var csv in csvArray) + { + var items = csv.Split(','); + if (items.Length != 2) + { + resultMessage = $"要素数が{items.Length}個のCSVあり[CSV:{csv}]"; + return false; + } + + // IDの数値チェック + if (!int.TryParse(items[1], out int id)) + { + resultMessage = $"IDが数値でないCSVあり[CSV:{csv}]"; + return false; + } + + // Nameのカンマ記号復元 + items[0] = items[0].Replace("@@@COMMA@@@", ","); + + dic[items[0]] = id; + } + return true; + } + } +} diff --git a/Commons/EncodingUxer.cs b/Commons/EncodingUxer.cs new file mode 100644 index 0000000..7c3381d --- /dev/null +++ b/Commons/EncodingUxer.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public class EncodingUxer + { + #region 列挙体 + + public enum EncodingType + { + Utf8WithBom, + Utf8WithoutBom, + ShiftJIS + } + + #endregion + + public static Encoding GetEncoding(EncodingType type) + { + switch (type) + { + case EncodingType.Utf8WithBom: + return new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); // BOMあり + case EncodingType.Utf8WithoutBom: + return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); // BOMなし + case EncodingType.ShiftJIS: + return Encoding.GetEncoding("shift_jis"); // ANSI(Windows-932) + default: + throw new ArgumentOutOfRangeException(nameof(type), "未対応のエンコーディングです"); + } + } + + public static EncodingType? GetEncodingType(string typeString) + { + typeString = typeString.ToLower(); + + if (StringUxer.WildcardMatch(typeString, "*utf*8*without*bom*")) + return EncodingType.Utf8WithoutBom; + if (StringUxer.WildcardMatch(typeString, "*utf*8*with*bom*")) + return EncodingType.Utf8WithBom; + if (StringUxer.WildcardMatch(typeString, "*utf*8*")) + return EncodingType.Utf8WithoutBom; + if (StringUxer.WildcardMatch(typeString, "*s*jis*")) + return EncodingType.ShiftJIS; + if (StringUxer.WildcardMatch(typeString, "*シ*jis*")) + return EncodingType.ShiftJIS; + return null; + } + } +} diff --git a/Commons/IntUxer.cs b/Commons/IntUxer.cs new file mode 100644 index 0000000..3c5a28c --- /dev/null +++ b/Commons/IntUxer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public static class IntUxer + { + public static int StrToInt(string str, int defaultValue) + { + if (int.TryParse(str, out int intValue)) + { + return intValue; + } + else + { + return defaultValue; + } + } + } +} diff --git a/Commons/LoggerUxer.cs b/Commons/LoggerUxer.cs new file mode 100644 index 0000000..db60004 --- /dev/null +++ b/Commons/LoggerUxer.cs @@ -0,0 +1,81 @@ +using KssSmaPlaLib.Ini; +using Serilog; +using Serilog.Events; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace KssSmaPlaLib.Commons +{ + /// + /// ロガー + /// + public static class LoggerUxer + { + /// + /// 初期化 + /// + /// ログINIファイルパス + public static void Initialize(string iniPath) + { + EnsureIniExists(iniPath); + var config = IniReferenceResolveUxer.ReadSectionWithReference(iniPath, "Logging"); + + var level = Enum.TryParse(config["LogLevel"], out LogEventLevel parsedLevel) + ? parsedLevel + : LogEventLevel.Information; + + var path = config["LogFilePath"]; + var rolling = config["RollingInterval"]; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(level) + .WriteTo.Console() + .WriteTo.File(path, rollingInterval: (RollingInterval)Enum.Parse(typeof(RollingInterval), rolling)) + .CreateLogger(); + } + + public static void Info(string message) => Log.Information(message); + public static void Warn(string message) => Log.Warning(message); + public static void Error(string message) => Log.Error(message); + public static void Close() => Log.CloseAndFlush(); + + public static void StartInfo() + { + // DLL(自身)のアセンブリ + var dllAssembly = Assembly.GetExecutingAssembly(); + var dllVersion = dllAssembly.GetName().Version?.ToString(); + var dllProduct = dllAssembly.GetCustomAttribute()?.Product ?? "不明"; + + // EXE(呼び出し元)のアセンブリ + var exeAssembly = Assembly.GetEntryAssembly(); + var exeVersion = exeAssembly?.GetName().Version?.ToString(); + var exeProduct = exeAssembly?.GetCustomAttribute()?.Product ?? "不明"; + + // ログ出力 + Info($"[EXE] 製品名: {exeProduct}, バージョン: {exeVersion}"); + Info($"[DLL] 製品名: {dllProduct}, バージョン: {dllVersion}"); + } + + /// + /// ログINI初期化(無ければ) + /// + /// ログINIファイルパス + private static void EnsureIniExists(string path) + { + if (!File.Exists(path)) + { + File.WriteAllLines(path, new[] + { + "[Logging]", + "LogLevel=Information", + "LogFilePath=log.txt", + "RollingInterval=Day" + }); + } + } + } +} diff --git a/Commons/RetCodeUxer.cs b/Commons/RetCodeUxer.cs new file mode 100644 index 0000000..41f70f0 --- /dev/null +++ b/Commons/RetCodeUxer.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public static class RetCodeUxer + { + /// + /// 戻り値(プロセス終了値) + /// + public enum RetCode + { + /// + /// 成功 + /// + Success = 0, + + /// + /// ファイルバックアップ時エラー + /// + FileBackupError = -1, + + /// + /// ファイルクリーンナップエラー + /// + CleanupError = -2, + + /// + /// 開始日エラー + /// + StartDateError = -3, + + /// + /// DB接続エラー + /// + DbConnectError = -4, + + /// + /// SQLフォルダパス未設定 + /// + SqlFolderPathNoSetting = -5, + + /// + /// SQLファイルロードエラー + /// + SqlFileLoadError = -6, + + /// + /// CSVファイル名パターン群が未設定 + /// + CsvFileNamePatternsNotSetting = -7, + + /// + /// 設定ファイルの要素数エラー + /// + IniFileItemCountError = -8, + + /// + /// 挿入前の削除時に例外発生 + /// + DeleteBeforeInsertException = -9, + + /// + /// CSVデータをDB登録時エラー + /// + InsertDbFromCsvFilesError = -10, + + /// + /// CSVからDB登録時に例外発生 + /// + CsvToDbException = -11, + + /// + /// ファイル削除でフォルダパス未設定 + /// + FileDeleteFolderPathNotSetting = -12, + + /// + /// ファイル削除でフォルダパス未存在 + /// + FileDeleteFolderPathNotExist = -13, + + /// + /// ファイル削除でファイル名パターン未設定 + /// + FileDeleteFileNamePatternNotSetting = -14, + + /// + /// ファイル削除でファイル名パターン削除エラー + /// + FileDeletePatternFilesError = -15, + + /// + /// SFTPオブジェクト生成エラー + /// + SftpObjectCreateError = -16, + + /// + /// SFTPアップロードエラー + /// + SftpUploadError = -17, + + /// + /// SFTPアップロードで入力フォルダ未存在 + /// + SftpInFolderPathNotExist = -18, + + /// + /// SFTPでCSVファイル名パターンが未設定 + /// + SftpCsvFileNamePatternsNotSetting = -19, + + /// + /// SFTPダウンロード時に例外発生 + /// + SftpDownloadException = -20, + + /// + /// SFTPファイル削除エラー + /// + SftpDeleteMatchingFilesError = -21, + + /// + /// SFTPパターンマッチファイルダウンロード時エラー + /// + SftpMatchFilesDownloadError = -22, + + /// + /// SFTP共通:テスト接続エラー + /// + SftpTestConnectError = -23, + } + } +} diff --git a/Commons/StringUxer.cs b/Commons/StringUxer.cs new file mode 100644 index 0000000..dd0d972 --- /dev/null +++ b/Commons/StringUxer.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public class StringUxer + { + public static string CreateACarInBikesString( + string bike, + string car, + string f1, + int count, + int luckyCount) + { + // 1. まず100個のバイクのリストを作成 + List list = Enumerable.Repeat(bike, count).ToList(); + + // 2. 0〜99のランダムな位置を1つ選ぶ + Random rand = new Random(); + int randomIndex = rand.Next(0, count); + + // 3. その位置を車に置き換える(ラッキー時はF1) + Random rand2 = new Random(); + var lucky = rand2.Next(0, luckyCount) == 0; + list[randomIndex] = lucky ? f1 : car; + + // 4. 文字列に結合 + return string.Concat(list); + } + + public static bool WildcardMatch(string input, string pattern) + { + if (string.IsNullOrEmpty(pattern)) return string.IsNullOrEmpty(input); + // "*" を ".*" に変換し、正規表現にする + string regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$"; + return Regex.IsMatch(input, regexPattern, RegexOptions.IgnoreCase); + } + public static bool TryParseIniSignature(string iniValue, out string functionName, out List args) + { + functionName = null; + args = null; + + var pattern = @"^([a-zA-Z_][a-zA-Z0-9_]*)\((.*?)\)$"; + var match = Regex.Match(iniValue.Trim(), pattern); + + if (!match.Success) + return false; + + functionName = match.Groups[1].Value; + var argsString = match.Groups[2].Value; + + args = string.IsNullOrEmpty(argsString) + ? new List() + : argsString.Split(',').Select(arg => arg.Trim()).ToList(); + + return true; + } + + public static bool TryExtractYearFromString(string str, out int year) + { + // var match = Regex.Match(str, @"\b(20\d{2})\b"); // 条件厳しめ + var match = Regex.Match(str, @"(20\d{2})"); // 条件緩め + year = match.Success ? int.Parse(match.Groups[1].Value) : -1; + return year != -1; + } + + /// + /// CSVから文字列取得(インデックス指定:範囲外の時既定値) + /// + /// CSV文字列(カンマ区切り) + /// インデックス + /// 範囲外の時の既定値 + /// 取得した文字列 + public static string GetStringByIndexFormCsv(string csv, int index, string noString = "(No name...)") + { + string[] strSplit = csv.Split(','); + if (index < 0 || index > strSplit.Length - 1) return noString; + return strSplit[index]; + } + + /// + /// 文字列中の改行コードのカット + /// + /// + /// + public static string CutKaigyo(string str) + { + return Regex.Replace(str, + // '\r'と'\n'とUnicode改行文字 + // (\u2028 (行区切り文字) + //  \u2029 (段落区切り文字) + //  \u0085 (次行文字)) + // の正規表現 + @"\r\n|[\r\n\u2028\u2029\u0085]", + "", + // 複数回行う時のためにコンパイルしておく + RegexOptions.Compiled); + } + } +} diff --git a/Commons/VariableUxer.cs b/Commons/VariableUxer.cs new file mode 100644 index 0000000..f563e7e --- /dev/null +++ b/Commons/VariableUxer.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Commons +{ + public static class VariableUxer + { + private static List _args = new List(); + + public static List Args { get => _args; } + public static void SetArgs(string[] args) { _args = args.ToList(); } + } +} diff --git a/Communications/SftpUxer.cs b/Communications/SftpUxer.cs new file mode 100644 index 0000000..c0631a3 --- /dev/null +++ b/Communications/SftpUxer.cs @@ -0,0 +1,302 @@ +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Encryption; +using KssSmaPlaLib.IO.File; +using Renci.SshNet; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Communications +{ + public class SftpUxer + { + public const string INIDIC_KEY_HOST = "Host"; + public const string INIDIC_KEY_PORT = "Port"; + public const string INIDIC_KEY_USERNAME = "UserName"; + public const string INIDIC_KEY_PRIVATE_KEY_FILE_PATH = "PrivateKeyFilePath"; + public const string INIDIC_KEY_PASS_PHRASE = "PassPhrase"; + + private readonly string _host; + private readonly int _port; + private readonly string _username; + private readonly string _privateKeyPath; // PuTTY形式でもOK + private readonly string _passPhrase; + + public static bool TryCreateByDic(Dictionary iniDic, out SftpUxer sftp, out string resultMessage) + { + sftp = null; + resultMessage = string.Empty; + + // ホスト名 + var host = DictionaryUxer.GetValueOrDefault(iniDic, INIDIC_KEY_HOST, string.Empty); + if (string.IsNullOrEmpty(host)) + { + resultMessage = $"SFTPのホスト名が指定されていません。[キー:{INIDIC_KEY_HOST}]"; + return false; + } + + // ポート番号 + var strPort = DictionaryUxer.GetValueOrDefault(iniDic, INIDIC_KEY_PORT, string.Empty); + if (int.TryParse(strPort, out var portNo) == false) + { + LoggerUxer.Error($"SFTPのポート番号が未指定または数値でありません。[キー:{INIDIC_KEY_PORT},ポート番号:{strPort}]"); + return false; + } + + // ユーザ名 + var username = DictionaryUxer.GetValueOrDefault(iniDic, INIDIC_KEY_USERNAME, string.Empty); + if (string.IsNullOrEmpty(username)) + { + LoggerUxer.Error($"SFTPのユーザ名が指定されていません。[キー:{INIDIC_KEY_USERNAME}]"); + return false; + } + + // プライベートキーファイルパス + var privateKeyPath = DictionaryUxer.GetValueOrDefault(iniDic, INIDIC_KEY_PRIVATE_KEY_FILE_PATH, string.Empty); + if (string.IsNullOrEmpty(privateKeyPath)) + { + LoggerUxer.Error($"SFTPのプライベートキーファイルパスが指定されていません。[キー:{INIDIC_KEY_PRIVATE_KEY_FILE_PATH}]"); + return false; + } + if (!File.Exists(privateKeyPath)) + { + LoggerUxer.Error($"SFTPのプライベートキーファイルパスが存在しません。[キー:{INIDIC_KEY_PRIVATE_KEY_FILE_PATH},パス:{privateKeyPath}]"); + return false; + } + + // パスフレーズ + var passPhrase = DictionaryUxer.GetValueOrDefault(iniDic, INIDIC_KEY_PASS_PHRASE, string.Empty); + + // ↓パスフレーズなしの場合もあり(必須としない) + //if (string.IsNullOrEmpty(passPhrase)) + //{ + // LoggerUxer.Error($"SFTPのパスフレーズが指定されていません。[キー:{INIDIC_KEY_PASS_PHRASE}]"); + // return false; + //} + + var passPhraseDec = string.Empty; + + if (!string.IsNullOrEmpty(passPhrase)) + { + // パスフレーズの復号化 + if (!SimpleEncryptUxer.Decrypt(passPhrase, out passPhraseDec, out string messageForMe)) + { + LoggerUxer.Error($"SFTPのパスフレーズ復号化で失敗しました。[キー:{INIDIC_KEY_PASS_PHRASE},エラー内容:{messageForMe}]"); + return false; + } + } + + // オブジェクト生成 + sftp = new SftpUxer(host, portNo, username, privateKeyPath, passPhraseDec); + + return true; + } + + private SftpUxer(string host, int port, string username, string privateKeyPath, string passPhrase) + { + _host = host; + _port = port; + _username = username; + _privateKeyPath = privateKeyPath; + _passPhrase = passPhrase; + } + + public bool TestConnect(out string resultMessage) + { + resultMessage = string.Empty; + try + { + PrivateKeyFile keyFile; + if(!string.IsNullOrEmpty(_passPhrase)) + { + // パスフレーズあり + keyFile = new PrivateKeyFile(_privateKeyPath, _passPhrase); + } + else + { + // パスフレーズなし + keyFile = new PrivateKeyFile(_privateKeyPath); + } + + var keyFiles = new[] { keyFile }; + + using (var client = new SftpClient(_host, _port, _username, keyFiles)) + { + client.Connect(); + client.Disconnect(); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"SFTPのテスト接続に失敗しました。[ホスト:{_host},ポート:{_port},ユーザ名:{_username},プライベートキーファイルパス:{_privateKeyPath},エラー内容:{ex.Message}]"; + return false; + } + } + + public bool Upload(List filePathList, string remotePath, out string resultMessage) + { + resultMessage = string.Empty; + if (string.IsNullOrEmpty(remotePath)) remotePath = "."; + + try + { + if (!remotePath.EndsWith("/")) remotePath += "/"; + + PrivateKeyFile keyFile; + if (!string.IsNullOrEmpty(_passPhrase)) + { + keyFile = new PrivateKeyFile(_privateKeyPath, _passPhrase); + } + else + { + keyFile = new PrivateKeyFile(_privateKeyPath); + } + var keyFiles = new[] { keyFile }; + + using (var client = new SftpClient(_host, _port, _username, keyFiles)) + { + client.Connect(); + foreach (var filePath in filePathList) + { + using (var fileStream = new FileStream(filePath, FileMode.Open)) + { + client.UploadFile(fileStream, remotePath + Path.GetFileName(filePath)); + } + } + client.Disconnect(); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"SFTPのアップロードでエラーが発生しました。[ファイル:{string.Join(",", filePathList)},リモートパス:{remotePath},エラー内容:{ex.Message}]"; + return false; + } + } + + public bool Download(string remotePath, string localPath, out string resultMessage) + { + resultMessage = string.Empty; + try + { + var keyFile = new PrivateKeyFile(_privateKeyPath); + var keyFiles = new[] { keyFile }; + + using (var client = new SftpClient(_host, _port, _username, keyFiles)) + { + client.Connect(); + using (var fileStream = new FileStream(localPath, FileMode.Create)) + { + client.DownloadFile(remotePath, fileStream); + } + client.Disconnect(); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"SFTPのダウンロード(Download)でエラーが発生しました。[エラー内容:{ex.Message}]"; + return false; + } + } + + public bool DownloadMatchingFiles( + string remoteDir, + string localDir, + out int downloadedFileCount, + out string resultMessage, + string pattern = "*.txt") + { + downloadedFileCount = 0; + resultMessage = string.Empty; + + if (string.IsNullOrEmpty(remoteDir)) remoteDir = "."; + if (string.IsNullOrEmpty(localDir)) localDir = "."; + + if (!Directory.Exists(localDir)) + { + resultMessage = $"ダウンロード先のローカルフォルダが存在しません。[ローカルフォルダ:{localDir}]"; + return false; + } + + try + { + PrivateKeyFile keyFile; + if (!string.IsNullOrEmpty(_passPhrase)) + { + keyFile = new PrivateKeyFile(_privateKeyPath, _passPhrase); + } + else + { + keyFile = new PrivateKeyFile(_privateKeyPath); + } + + using (var client = new SftpClient(_host, _port, _username, keyFile)) + { + client.Connect(); + + // リモートディレクトリ内のファイル一覧を取得 + var files = client.ListDirectory(remoteDir) + .Where(f => !f.IsDirectory && StringUxer.WildcardMatch(f.Name, pattern)); + + foreach (var file in files) + { + string localPath = Path.Combine(localDir, file.Name); + using (var fs = new FileStream(localPath, FileMode.Create)) + { + client.DownloadFile(file.FullName, fs); + Console.WriteLine($"1ファイルダウンロードしました。[ファイル名:{file.Name}]"); + downloadedFileCount++; + } + } + client.Disconnect(); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"SFTPのダウンロード(DownloadMatchingFiles)でエラーが発生しました。[エラー内容:{ex.Message}]"; + return false; + } + } + public bool DeleteMatchingFiles( + string remoteDir, + out int deletedFileCount, + out string resultMessage, + string pattern = "*.txt") + { + deletedFileCount = 0; + resultMessage = string.Empty; + try + { + var keyFile = new PrivateKeyFile(_privateKeyPath, _passPhrase); + using (var client = new SftpClient(_host, _port, _username, keyFile)) + { + client.Connect(); + + // リモートディレクトリ内のファイル一覧を取得 + var files = client.ListDirectory(remoteDir) + .Where(f => !f.IsDirectory && StringUxer.WildcardMatch(f.Name, pattern)); + + foreach (var file in files) + { + string remotePath = PathUxer.CombineUnixPath(remoteDir, file.Name); + client.DeleteFile(remotePath); + deletedFileCount++; + } + client.Disconnect(); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"SFTPのファイル削除でエラーが発生しました。[エラー内容:{ex.Message}]"; + return false; + } + } + } +} diff --git a/DtoMapping/Attributes/ExcelColumnLetterAttribute.cs b/DtoMapping/Attributes/ExcelColumnLetterAttribute.cs new file mode 100644 index 0000000..1d6b304 --- /dev/null +++ b/DtoMapping/Attributes/ExcelColumnLetterAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.DtoMapping.Attributes +{ + /// + /// Excel列レター属性(プロパティに付与) + /// + [AttributeUsage(AttributeTargets.Property)] + public class ExcelColumnLetterAttribute : Attribute + { + /// + /// Excel対応列レター + /// + public string ColumnLetter { get; } + + public ExcelColumnLetterAttribute(string columnLetter) + { + ColumnLetter = columnLetter; + } + } +} diff --git a/Encryption/FIleEncryptUxer.cs b/Encryption/FIleEncryptUxer.cs new file mode 100644 index 0000000..adaa866 --- /dev/null +++ b/Encryption/FIleEncryptUxer.cs @@ -0,0 +1,271 @@ +using KssSmaPlaLib.Commons; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Encryption +{ + public static class FIleEncryptUxer + { + public static bool TuckIntoMain(string[] args) + { + if (args.Length < 2) return false; + + if (args[0].ToLower() == "/encfile") + { + var srcPath = args[1]; + var descPath = args.Length >= 3 ? args[2] : args[1] + ".enc"; + var password= args.Length >= 4 ? args[3] : string.Empty; + if (TryEncrypt(srcPath, descPath, out var messageForMe, password)) + { + LoggerUxer.Info($"ファイル暗号化が成功しました。"); + } + else + { + LoggerUxer.Error($"ファイル暗号化が失敗しました。[エラー内容:{messageForMe}]"); + } + return true; + } + else if (args[0].ToLower() == "/decfile") + { + var srcPath = args[1]; + var descPath = args.Length >= 3 ? args[2] : (srcPath.EndsWith(".enc") ? srcPath.TrimEnd(".enc".ToArray()) : srcPath + ".src"); + var password = args.Length >= 4 ? args[3] : string.Empty; + if (TryDecrypt(srcPath, descPath, out var messageForMe, password)) + { + Console.WriteLine($"ファイル複合化が成功しました。"); + } + else + { + Console.WriteLine($"ファイル複合化が失敗しました。[エラー内容:{messageForMe}]"); + } + return true; + } + else if (args[0].ToLower() == "/encbigfile") + { + var srcPath = args[1]; + var descPath = args.Length >= 3 ? args[2] : args[1] + ".enc"; + var password = args.Length >= 4 ? args[3] : string.Empty; + // テキストのみ方式 + if (TryEncryptLargeFileToBase64(srcPath, descPath, out var messageForMe, true, password)) + { + LoggerUxer.Info($"ファイル暗号化が成功しました。"); + } + else + { + LoggerUxer.Error($"ファイル暗号化が失敗しました。[エラー内容:{messageForMe}]"); + } + return true; + } + else if (args[0].ToLower() == "/decbigfile") + { + var srcPath = args[1]; + var descPath = args.Length >= 3 ? args[2] : (srcPath.EndsWith(".enc") ? srcPath.TrimEnd(".enc".ToArray()) : srcPath + ".src"); + var password = args.Length >= 4 ? args[3] : string.Empty; + if (TryDecryptLargeFileFromBase64(srcPath, descPath, out var messageForMe, true, password)) + { + Console.WriteLine($"ファイル複合化が成功しました。"); + } + else + { + Console.WriteLine($"ファイル複合化が失敗しました。[エラー内容:{messageForMe}]"); + } + return true; + } + return false; + } + + public static bool TryEncryptLargeFileToBase64(string inputFilePath, string outputTextFilePath, out string resultMessage, bool isAllText = false, string password = "") + { + if (string.IsNullOrEmpty(password)) password = SimpleEncryptUxer.FixedPassword; + + try + { + byte[] key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password)); + using (var aes = Aes.Create()) + { + aes.Key = key; + aes.GenerateIV(); + + using (var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) + using (var outputStream = new FileStream(outputTextFilePath, FileMode.Create, FileAccess.Write)) + { + if (isAllText) + { + using (var memoryStream = new MemoryStream()) + using (var encryptor = aes.CreateEncryptor()) + using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) + { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0) + { + cryptoStream.Write(buffer, 0, bytesRead); + } + cryptoStream.FlushFinalBlock(); + + string ivBase64 = Convert.ToBase64String(aes.IV); + string cipherBase64 = Convert.ToBase64String(memoryStream.ToArray()); + string outputText = $"{ivBase64}::{cipherBase64}"; + + byte[] outputBytes = Encoding.UTF8.GetBytes(outputText); + outputStream.Write(outputBytes, 0, outputBytes.Length); + } + } + else + { + // IVはバイナリで先頭に書き込む + outputStream.Write(aes.IV, 0, aes.IV.Length); + + using (var base64Transform = new ToBase64Transform()) + using (var cryptoStream = new CryptoStream(outputStream, base64Transform, CryptoStreamMode.Write)) + using (var encryptor = aes.CreateEncryptor()) + using (var aesStream = new CryptoStream(cryptoStream, encryptor, CryptoStreamMode.Write)) + { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0) + { + aesStream.Write(buffer, 0, bytesRead); + } + aesStream.FlushFinalBlock(); + } + } + } + } + resultMessage = "暗号化成功"; + return true; + } + catch (Exception ex) + { + resultMessage = $"暗号化失敗: {ex.Message}"; + return false; + } + } + + public static bool TryDecryptLargeFileFromBase64(string inputTextFilePath, string outputFilePath, out string resultMessage, bool isAllText = false, string password = "") + { + resultMessage = string.Empty; + + if (string.IsNullOrEmpty(password)) password = SimpleEncryptUxer.FixedPassword; + + try + { + byte[] key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password)); + using (var aes = Aes.Create()) + { + aes.Key = key; + + if (isAllText) + { + string inputText = File.ReadAllText(inputTextFilePath, Encoding.UTF8); + string[] parts = inputText.Split(new[] { "::" }, StringSplitOptions.None); + + if (parts.Length != 2) + { + resultMessage = $"暗号ファイルの形式が不正です(::区切りが見つかりません)"; + return false; + } + + byte[] iv = Convert.FromBase64String(parts[0]); + byte[] cipherBytes = Convert.FromBase64String(parts[1]); + + aes.IV = iv; + + using (var decryptor = aes.CreateDecryptor()) + using (var memoryStream = new MemoryStream(cipherBytes)) + using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) + using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = cryptoStream.Read(buffer, 0, buffer.Length)) > 0) + { + outputStream.Write(buffer, 0, bytesRead); + } + } + } + else + { + using (var inputStream = new FileStream(inputTextFilePath, FileMode.Open, FileAccess.Read)) + { + byte[] iv = new byte[16]; + inputStream.Read(iv, 0, iv.Length); + aes.IV = iv; + + using (var base64Transform = new FromBase64Transform()) + using (var cryptoStream = new CryptoStream(inputStream, base64Transform, CryptoStreamMode.Read)) + using (var decryptor = aes.CreateDecryptor()) + using (var aesStream = new CryptoStream(cryptoStream, decryptor, CryptoStreamMode.Read)) + using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = aesStream.Read(buffer, 0, buffer.Length)) > 0) + { + outputStream.Write(buffer, 0, bytesRead); + } + } + } + } + } + return true; + } + catch (Exception ex) + { + resultMessage = $"ファイルの復号化に失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + + public static bool TryEncrypt(string inputFilePath, string outputTextFilePath, out string resultMessage, string password = "") + { + try + { + string base64Data = Convert.ToBase64String(File.ReadAllBytes(inputFilePath)); + + bool success = SimpleEncryptUxer.Encrypt(base64Data, out var resultText, out var messageForMe, password); + resultMessage = messageForMe; + + if (success) + { + File.WriteAllText(outputTextFilePath, resultText, Encoding.UTF8); + return true; + } + return false; + } + catch (Exception ex) + { + resultMessage = $"ファイル暗号化失敗: {ex.Message}"; + return false; + } + } + public static bool TryDecrypt(string inputTextFilePath, string outputBinaryFilePath, out string resultMessage, string password = "") + { + try + { + string encryptedText = File.ReadAllText(inputTextFilePath, Encoding.UTF8); + + bool success = SimpleEncryptUxer.Decrypt(encryptedText, out var resultText, out var messageForMe, password); + resultMessage = messageForMe; + + if (success) + { + byte[] binaryData = Convert.FromBase64String(resultText); + File.WriteAllBytes(outputBinaryFilePath, binaryData); + return true; + } + return false; + } + catch (Exception ex) + { + resultMessage = $"ファイル復号化失敗: {ex.Message}"; + return false; + } + } + } +} diff --git a/Encryption/SimpleEncryptUxer.cs b/Encryption/SimpleEncryptUxer.cs new file mode 100644 index 0000000..9090d35 --- /dev/null +++ b/Encryption/SimpleEncryptUxer.cs @@ -0,0 +1,96 @@ +using DocumentFormat.OpenXml.Wordprocessing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Encryption +{ + public static class SimpleEncryptUxer + { + public static readonly string FixedPassword = "KssSmaPlaDxer!"; + private static readonly byte[] FixedIV = new byte[16]; + + public static bool TuckIntoMain(string[] args) + { + if (args.Length != 2) return false; + + if (args[0].ToLower() == "/enc") + { + if (SimpleEncryptUxer.Encrypt(args[1], out var resultText, out var resultMessage)) + { + Console.WriteLine($"暗号化結果[{resultText}]"); + } + else + { + Console.WriteLine(resultMessage); + } + return true; + } + else if (args[0].ToLower() == "/dec") + { + if (SimpleEncryptUxer.Decrypt(args[1], out var resultText, out var resultMessage)) + { + Console.WriteLine($"複合化結果[{resultText}]"); + } + else + { + Console.WriteLine(resultMessage); + } + return true; + } + return false; + } + + public static bool Encrypt(string plainText, out string resultText, out string resultMessage, string password = "") + { + resultText = string.Empty; + resultMessage = string.Empty; + password = string.IsNullOrEmpty(password) ? FixedPassword : password; + try + { + byte[] key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password)); + // 固定IV(簡易仕様とする)※IVはオールゼロ + using (var aes = new AesManaged { Key = key, IV = FixedIV }) + using (var encryptor = aes.CreateEncryptor()) + { + byte[] inputBytes = Encoding.UTF8.GetBytes(plainText); + byte[] encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); + resultText = Convert.ToBase64String(encryptedBytes); + return true; + } + } + catch (Exception ex) + { + resultMessage = $"暗号化に失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + + public static bool Decrypt(string encryptedText, out string resultText, out string resultMessage, string password = "") + { + resultText = string.Empty; + resultMessage = string.Empty; + password = string.IsNullOrEmpty(password) ? FixedPassword : password; + try + { + byte[] key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password)); + byte[] encryptedBytes = Convert.FromBase64String(encryptedText); + using (var aes = new AesManaged { Key = key, IV = FixedIV }) + using (var decryptor = aes.CreateDecryptor()) + { + byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length); + resultText = Encoding.UTF8.GetString(decryptedBytes); + return true; + } + } + catch (Exception ex) + { + resultMessage = $"複合化に失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + } +} diff --git a/Exchange/CsvDbSyncUxer.cs b/Exchange/CsvDbSyncUxer.cs new file mode 100644 index 0000000..edab912 --- /dev/null +++ b/Exchange/CsvDbSyncUxer.cs @@ -0,0 +1,153 @@ +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.IO.Database; +using KssSmaPlaLib.IO.File; +using Org.BouncyCastle.Asn1.X509; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Exchange +{ + public static class CsvDbSyncUxer + { + private const string INI_KEY_INSERT_PROCESSING_MODE = "InsertProcessingMode"; + private const string INI_VALUE_INSERT_PROCESSING_MODE_BULK = "Bulk"; + private const string INI_VALUE_INSERT_PROCESSING_MODE_ROW_BY_ROW = "RowByRow"; + private const string INI_KEY_SQL_NAME_INSERT_BULK = "SqlNameInsertBulk"; + + + public static bool InsertDbFromCsvFiles( + Dictionary iniDic, + DbUxer con, + string csvFilePath, + string dupCheckSql, + string insertSqlName, + out string resultMessage, + EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom) + { + var messageForMe = string.Empty; + + resultMessage = string.Empty; + + var mode = DictionaryUxer.GetValueOrDefault( + iniDic, + INI_KEY_INSERT_PROCESSING_MODE, + INI_VALUE_INSERT_PROCESSING_MODE_ROW_BY_ROW); + //if (string.IsNullOrEmpty(mode)) + //{ + // resultMessage = $"挿入モードが設定されていません。[キー:{INI_KEY_INSERT_PROCESSING_MODE}]"; + // return false; + //} + + string[] validValues = { + INI_VALUE_INSERT_PROCESSING_MODE_BULK, + INI_VALUE_INSERT_PROCESSING_MODE_ROW_BY_ROW }; + + mode = mode.Trim(); + + if (!validValues.Contains(mode, StringComparer.OrdinalIgnoreCase)) + { + resultMessage = $"挿入モードの値が不正です。[{INI_KEY_INSERT_PROCESSING_MODE}:{mode}]"; + return false; + } + + if (mode.ToUpper() == INI_VALUE_INSERT_PROCESSING_MODE_ROW_BY_ROW.ToUpper()) + { + // 1行ずつ挿入(従来方式) + + // CSV読み込み + if (!CsvUxer.ImportFromCsv(csvFilePath, out var rs, out messageForMe, encodingType)) + { + resultMessage = messageForMe; + return false; + } + + LoggerUxer.Info($"[CSV件数:{rs.Count}]"); + + if (string.IsNullOrEmpty(dupCheckSql)) + { + LoggerUxer.Info($"重複チェックSQLが未指定のため重複チェックは行いません。"); + } + else + { + LoggerUxer.Info($"重複チェックを行います。[SQLファイル:{dupCheckSql}]"); + } + + foreach (var rec in rs) + { + var recObj = rec.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value); + + if (!string.IsNullOrEmpty(dupCheckSql)) + { + // 重複チェック + if (!con.ExecuteQuery(dupCheckSql, recObj, out var dupRs, out messageForMe)) + { + resultMessage = "重複チェックエラー:" + messageForMe; + return false; + } + if (dupRs.Count != 1) + { + resultMessage = $"重複チェックで取得件数エラーです。[取得件数:{dupRs.Count}]"; + return false; + } + // ↓COUNT(*)の結果型はintにあらず(ポスグレの場合long)ゆえに文字列化してint化する + if (!int.TryParse(dupRs[0].Values.ElementAt(0).ToString(), out var intCnt)) + { + resultMessage = $"重複チェックで取得値(件数)が数値ではありません。[取得値:{dupRs[0].Values.ElementAt(0)}]"; + return false; + } + // 重複レコードありの場合、飛ばす + if (intCnt > 0) + { + LoggerUxer.Info($"重複レコードのため、飛ばします。[該当件数:{intCnt}]"); + continue; + } + LoggerUxer.Info($"重複チェックにて重複レコードは見つかりませんでした。"); + } + + // DB挿入 + if (!string.IsNullOrEmpty(insertSqlName)) + { + LoggerUxer.Info($"[DB挿入SQL名:{insertSqlName}]"); + if (!con.ExecuteNonQuery(insertSqlName, recObj, out var resultCount, out messageForMe)) + { + resultMessage = messageForMe; + return false; + } + LoggerUxer.Info($"[DB挿入件数:{resultCount}]"); + } + else + { + LoggerUxer.Info($"DB挿入SQL名が未指定のためDB挿入は行いません。"); + } + } + } + else + { + // バルク挿入 + LoggerUxer.Info($"バルク挿入が指定されました。"); + + insertSqlName = DictionaryUxer.GetValueOrDefault( + iniDic, + INI_KEY_SQL_NAME_INSERT_BULK, + string.Empty); + if (string.IsNullOrEmpty(insertSqlName)) + { + resultMessage = $"バルク挿入SQLファイル名が指定されていません。[キー:{INI_KEY_SQL_NAME_INSERT_BULK}]"; + return false; + } + LoggerUxer.Info($"[DBバルク挿入SQL名:{insertSqlName}]"); + if (!con.BeginTextImport(insertSqlName, csvFilePath, out var resultCount, out messageForMe)) + { + resultMessage = messageForMe; + return false; + } + LoggerUxer.Info($"[DBバルク挿入件数:{resultCount}]"); + } + return true; + } + } +} diff --git a/Exchange/Interfaces/ICsvExportableUxer.cs b/Exchange/Interfaces/ICsvExportableUxer.cs new file mode 100644 index 0000000..ad07172 --- /dev/null +++ b/Exchange/Interfaces/ICsvExportableUxer.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Exchange.Interfaces +{ + public interface ICsvExportableUxer + { + string CsvHeader { get; } // ヘッダー行 + string ToCsvLine(); // データ行 + } +} diff --git a/Forms/ColorHelper.cs b/Forms/ColorHelper.cs new file mode 100644 index 0000000..9f4eeff --- /dev/null +++ b/Forms/ColorHelper.cs @@ -0,0 +1,74 @@ +using DocumentFormat.OpenXml.Wordprocessing; +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Color = System.Drawing.Color; + +namespace KssSmaPlaLib.Forms +{ + public static class ColorHelper + { + /// + /// 色選択 + /// + /// + /// + /// + /// + public static Color? SelectColor(Form form, Color defaultColor, string iniKey = "") + { + using (ColorDialog cd = new ColorDialog()) + { + // 今の色を初期選択にしておく + cd.Color = defaultColor; + + if (cd.ShowDialog(form) != DialogResult.OK) return null; + + if (!string.IsNullOrEmpty(iniKey)) + { + // 2. 🔥 Colorを16進数(#RRGGBB)に変換してINIに書き込み + string colorHex = ColorTranslator.ToHtml(cd.Color); + IniHelper.WriteValue(null, form.Name, iniKey, colorHex); + } + return cd.Color; + } + } + + /// + /// Colorを汎用的なHTML色コード文字列に変換する + /// + /// + /// + public static string ColorToHtmlColorString(Color color) + { + return ColorTranslator.ToHtml(color); + } + + /// + /// 汎用的なHTML色コード文字列からColorに変換する + /// + /// + /// + public static Color HtmlColorStringToColor(string htmlColorString) + { + return ColorTranslator.FromHtml(htmlColorString); + } + + /// + /// 背景色から最適な文字色(黒/白)を返すメソッド + /// + /// + /// + public static Color GetContrastColor(Color backColor) + { + // 輝度計算 (ITU-R BT.601) + double luminance = (0.299 * backColor.R) + (0.587 * backColor.G) + (0.114 * backColor.B); + return (luminance > 128) ? Color.Black : Color.White; + } + } +} diff --git a/Forms/ContextMenuFunction/FontUxer.cs b/Forms/ContextMenuFunction/FontUxer.cs new file mode 100644 index 0000000..5dde394 --- /dev/null +++ b/Forms/ContextMenuFunction/FontUxer.cs @@ -0,0 +1,74 @@ +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms.ContextMenuFunction +{ + public static class FontUxer + { + /// + /// フォント選択 + /// + /// + /// + /// + /// + /// + /// + public static bool SelectFont(Form form, string section, string key, Font defaultFont, out Font resultFont) + { + // デフォルト値 + resultFont = new Font(defaultFont, defaultFont.Style); + + using (var dlg = new FontDialog()) + { + var defaultFontCsv = + defaultFont.Name + "," + + defaultFont.Size.ToString() + "," + + defaultFont.Bold.ToString(); + var iniFontCsv = IniHelper.ReadValue(null, section, key, defaultFontCsv); + var fontItems = iniFontCsv.Split(','); + if (fontItems.Length != 3) fontItems = defaultFontCsv.Split(','); + if (float.TryParse(fontItems[1], out var size) == false) size = defaultFont.Size; + if (bool.TryParse(fontItems[2], out var bold) == false) bold = defaultFont.Bold; + + dlg.Font = new Font(fontItems[0], size, bold ? FontStyle.Bold : FontStyle.Regular); + dlg.AllowVerticalFonts = false; + // 文字飾り(取り消し線と下線)を表示させない + dlg.ShowEffects = false; + var result = dlg.ShowDialog(form); + if (result == DialogResult.Cancel) return false; + + // INI保存 + var fontCsv = + dlg.Font.Name + "," + + dlg.Font.Size.ToString() + "," + + dlg.Font.Bold.ToString(); + IniHelper.WriteValue(null, section, key, fontCsv); + + // 出力 + resultFont = new Font(dlg.Font, dlg.Font.Style); + return true; + } + } + public static Font GetFont(string section, string key, Font defaultFont) + { + var defaultFontCsv = + defaultFont.Name + "," + + defaultFont.Size.ToString() + "," + + defaultFont.Bold.ToString(); + + var iniFontCsv = IniHelper.ReadValue(null, section, key, defaultFontCsv); + var fontItems = iniFontCsv.Split(','); + if (fontItems.Length != 3) fontItems = defaultFontCsv.Split(','); + if (float.TryParse(fontItems[1], out var size) == false) size = defaultFont.Size; + if (bool.TryParse(fontItems[2], out var bold) == false) bold = defaultFont.Bold; + return new Font(fontItems[0], size, bold ? FontStyle.Bold : FontStyle.Regular); + } + } +} diff --git a/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.Designer.cs b/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.Designer.cs new file mode 100644 index 0000000..14d1423 --- /dev/null +++ b/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.Designer.cs @@ -0,0 +1,197 @@ +namespace KssSmaPlaLib.Forms.ContextMenuFunction.Gamen +{ + partial class DgvColumnEditorForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.ColumnDgv = new System.Windows.Forms.DataGridView(); + this.CancelButton2 = new System.Windows.Forms.Button(); + this.OkButton = new System.Windows.Forms.Button(); + this.colPropertyName = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colDefaultDisplayName = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colDisplayName = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.UpButton = new System.Windows.Forms.Button(); + this.DownButton = new System.Windows.Forms.Button(); + this.PopupMenu = new System.Windows.Forms.ContextMenuStrip(this.components); + this.EditDisplayNameMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.VisibleMenu = new System.Windows.Forms.ToolStripMenuItem(); + ((System.ComponentModel.ISupportInitialize)(this.ColumnDgv)).BeginInit(); + this.PopupMenu.SuspendLayout(); + this.SuspendLayout(); + // + // ColumnDgv + // + this.ColumnDgv.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ColumnDgv.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.ColumnDgv.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.colPropertyName, + this.colDefaultDisplayName, + this.colDisplayName}); + this.ColumnDgv.Location = new System.Drawing.Point(15, 16); + this.ColumnDgv.Margin = new System.Windows.Forms.Padding(6, 7, 6, 7); + this.ColumnDgv.Name = "ColumnDgv"; + this.ColumnDgv.RowHeadersWidth = 62; + this.ColumnDgv.RowTemplate.Height = 27; + this.ColumnDgv.Size = new System.Drawing.Size(505, 429); + this.ColumnDgv.TabIndex = 0; + this.ColumnDgv.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ColumnDgv_KeyDown); + // + // CancelButton2 + // + this.CancelButton2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelButton2.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelButton2.Location = new System.Drawing.Point(346, 455); + this.CancelButton2.Name = "CancelButton2"; + this.CancelButton2.Size = new System.Drawing.Size(177, 58); + this.CancelButton2.TabIndex = 1; + this.CancelButton2.Text = "キャンセル"; + this.CancelButton2.UseVisualStyleBackColor = true; + // + // OkButton + // + this.OkButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.OkButton.Location = new System.Drawing.Point(163, 455); + this.OkButton.Name = "OkButton"; + this.OkButton.Size = new System.Drawing.Size(177, 58); + this.OkButton.TabIndex = 2; + this.OkButton.Text = "OK"; + this.OkButton.UseVisualStyleBackColor = true; + this.OkButton.Click += new System.EventHandler(this.OkButton_Click); + // + // colPropertyName + // + this.colPropertyName.HeaderText = "識別名"; + this.colPropertyName.MinimumWidth = 8; + this.colPropertyName.Name = "colPropertyName"; + this.colPropertyName.Width = 150; + // + // colDefaultDisplayName + // + this.colDefaultDisplayName.HeaderText = "デフォ表示名"; + this.colDefaultDisplayName.MinimumWidth = 8; + this.colDefaultDisplayName.Name = "colDefaultDisplayName"; + this.colDefaultDisplayName.Width = 150; + // + // colDisplayName + // + this.colDisplayName.HeaderText = "表示名"; + this.colDisplayName.MinimumWidth = 8; + this.colDisplayName.Name = "colDisplayName"; + this.colDisplayName.Width = 150; + // + // UpButton + // + this.UpButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.UpButton.Location = new System.Drawing.Point(15, 455); + this.UpButton.Name = "UpButton"; + this.UpButton.Size = new System.Drawing.Size(59, 58); + this.UpButton.TabIndex = 3; + this.UpButton.Text = "👆"; + this.UpButton.UseVisualStyleBackColor = true; + this.UpButton.Click += new System.EventHandler(this.UpButton_Click); + // + // DownButton + // + this.DownButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.DownButton.Location = new System.Drawing.Point(80, 455); + this.DownButton.Name = "DownButton"; + this.DownButton.Size = new System.Drawing.Size(59, 58); + this.DownButton.TabIndex = 4; + this.DownButton.Text = "👇"; + this.DownButton.UseVisualStyleBackColor = true; + this.DownButton.Click += new System.EventHandler(this.DownButton_Click); + // + // PopupMenu + // + this.PopupMenu.Font = new System.Drawing.Font("メイリオ", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(128))); + this.PopupMenu.ImageScalingSize = new System.Drawing.Size(24, 24); + this.PopupMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.EditDisplayNameMenu, + this.VisibleMenu}); + this.PopupMenu.Name = "PopupMenu"; + this.PopupMenu.Size = new System.Drawing.Size(394, 133); + // + // EditDisplayNameMenu + // + this.EditDisplayNameMenu.Name = "EditDisplayNameMenu"; + this.EditDisplayNameMenu.Size = new System.Drawing.Size(393, 48); + this.EditDisplayNameMenu.Text = "表示名の編集..."; + this.EditDisplayNameMenu.Click += new System.EventHandler(this.EditDisplayNameMenu_Click); + // + // VisibleMenu + // + this.VisibleMenu.Name = "VisibleMenu"; + this.VisibleMenu.Size = new System.Drawing.Size(393, 48); + this.VisibleMenu.Text = "表示/非表示の切り替え"; + this.VisibleMenu.Click += new System.EventHandler(this.VisibleMenu_Click); + // + // DgvColumnEditorForm + // + this.AcceptButton = this.OkButton; + this.AutoScaleDimensions = new System.Drawing.SizeF(19F, 42F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.White; + this.CancelButton = this.CancelButton2; + this.ClientSize = new System.Drawing.Size(535, 525); + this.ContextMenuStrip = this.PopupMenu; + this.Controls.Add(this.DownButton); + this.Controls.Add(this.UpButton); + this.Controls.Add(this.OkButton); + this.Controls.Add(this.CancelButton2); + this.Controls.Add(this.ColumnDgv); + this.Font = new System.Drawing.Font("メイリオ", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(128))); + this.Margin = new System.Windows.Forms.Padding(6, 7, 6, 7); + this.Name = "DgvColumnEditorForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.Manual; + this.Text = "列一覧💖"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DgvColumnEditorForm_FormClosing); + this.Load += new System.EventHandler(this.DgvColumnEditorForm_Load); + ((System.ComponentModel.ISupportInitialize)(this.ColumnDgv)).EndInit(); + this.PopupMenu.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.DataGridView ColumnDgv; + private System.Windows.Forms.Button CancelButton2; + private System.Windows.Forms.Button OkButton; + private System.Windows.Forms.DataGridViewTextBoxColumn colPropertyName; + private System.Windows.Forms.DataGridViewTextBoxColumn colDefaultDisplayName; + private System.Windows.Forms.DataGridViewTextBoxColumn colDisplayName; + private System.Windows.Forms.Button UpButton; + private System.Windows.Forms.Button DownButton; + private System.Windows.Forms.ContextMenuStrip PopupMenu; + private System.Windows.Forms.ToolStripMenuItem EditDisplayNameMenu; + private System.Windows.Forms.ToolStripMenuItem VisibleMenu; + } +} \ No newline at end of file diff --git a/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.cs b/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.cs new file mode 100644 index 0000000..847eece --- /dev/null +++ b/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.cs @@ -0,0 +1,334 @@ +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Forms; +using KssSmaPlaLib.Ini; +using KssSmaPlaLib.Input.Gamen; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms.ContextMenuFunction.Gamen +{ + public partial class DgvColumnEditorForm : Form + { + private const string COL_NAME_PROPERTY_NAME = "colPropertyName"; + private const string COL_NAME_DEFAULT_DISPLAY_NAME = "colDefaultDisplayName"; + private const string COL_NAME_DISPLAY_NAME = "colDisplayName"; + + /// + /// 対象のDGV + /// + private DataGridView _targetDgv = null; + + /// + /// 対象DGVの列名から列インデックスを取得する辞書 + /// (後ほど列インデックスが全てゼロになる事象が起きるため) + /// + private readonly Dictionary _colNameToRowIndexDic = null; + + /// + /// コンストラクタ + /// + /// + public DgvColumnEditorForm(DataGridView dgv) + { + InitializeComponent(); + + _targetDgv = dgv; + + // 列名から列インデックスを取得する辞書の作成(後ほど列インデックスが全てゼロになる事象が起きるため) + _colNameToRowIndexDic = new Dictionary(); + foreach (DataGridViewColumn col in _targetDgv.Columns) _colNameToRowIndexDic.Add(col.Name, col.Index); + + this.DialogResult = DialogResult.Cancel; + + DgvHelper.Initialize(ColumnDgv); + + ColumnDgv.Rows.Clear(); + + // 編集した表示名 + var csv = IniHelper.ReadValue(null, _targetDgv.Parent.Name + "." + _targetDgv.Name, KEY_NAME_DISPLAY_NAME_CSV); + + // 表示順のリストを作る + var cols = new DataGridViewColumn[_targetDgv.Columns.Count]; + _targetDgv.Columns.CopyTo(cols, 0); + Array.Sort(cols, (a, b) => a.DisplayIndex.CompareTo(b.DisplayIndex)); + + foreach (var col in cols) + { + var index = ColumnDgv.Rows.Add(); + string propertyName = string.Empty; + string defaultDisplayName = string.Empty; + bool visible = true; + var colIndex = col.Index; + var displayIndex = col.DisplayIndex; + if (col.Tag == null || !(col.Tag is PropertyDescriptor)) + { + propertyName = col.Name; + defaultDisplayName = propertyName; + } + else + { + PropertyDescriptor pd = col.Tag as PropertyDescriptor; + propertyName = pd.Name; + defaultDisplayName = pd.DisplayName; + var browsableAttr = pd.Attributes.OfType().FirstOrDefault(); + if (browsableAttr != null && !browsableAttr.Browsable) + { + visible = false; + } + } + //string displayName = col.HeaderText; + string displayName = + StringUxer.GetStringByIndexFormCsv(csv, colIndex, string.Empty) + .Replace("@@@COMMA@@@", ","); + + ColumnDgv.Rows[index].Tag = col; + ColumnDgv.Rows[index].Cells[COL_NAME_PROPERTY_NAME].Value = (!visible ? "【非】" : "") + propertyName; + ColumnDgv.Rows[index].Cells[COL_NAME_DEFAULT_DISPLAY_NAME].Value = defaultDisplayName; + ColumnDgv.Rows[index].Cells[COL_NAME_DISPLAY_NAME].Value = displayName == defaultDisplayName ? "" : displayName; + + SetVisible(index, col.Visible); + } + + //// タグに入れておく(PropertyDescriptor型) + //col.Tag = pd; + + //// 列ヘッダーを [DisplayName] に + //col.HeaderText = pd.DisplayName; + + //// [Browsable(false)] の列は非表示(AutoGenerateでも列が立つ場合の保険) + //var browsableAttr = pd.Attributes.OfType().FirstOrDefault(); + //if (browsableAttr != null && !browsableAttr.Browsable) + //{ + // col.Visible = false; + // continue; + //} + + // DGV列幅復元 + DgvHelper.DgvLoadColWidth(ColumnDgv); + } + + /// + /// フォームロード + /// + /// + /// + private void DgvColumnEditorForm_Load(object sender, EventArgs e) + { + // バウンズ復元 + FormHelper.LoadFormBounds(this); + } + + /// + /// 👆ボタンクリック時 + /// + /// + /// + private void UpButton_Click(object sender, EventArgs e) + { + // 対象外状況判断 + if (ColumnDgv.SelectedRows.Count < 1) return; + if (ColumnDgv.SelectedRows[0].Index == 0) return; + + // 事前取得 + var row = ColumnDgv.SelectedRows[0]; + var index = row.Index; // Removeしたら-1になるからその前に取得しておく + + // 行移動の実行 + ColumnDgv.Rows.Remove(row); + ColumnDgv.Rows.Insert(index - 1, row); + + // 行選択 + ColumnDgv.ClearSelection(); + ColumnDgv.Rows[index - 1].Selected = true; + } + + /// + /// 👇ボタンクリック時 + /// + /// + /// + private void DownButton_Click(object sender, EventArgs e) + { + // 対象外状況判断 + if (ColumnDgv.SelectedRows.Count < 1) return; + if (ColumnDgv.SelectedRows[0].Index == ColumnDgv.RowCount - 1) return; + + // 事前取得 + var row = ColumnDgv.SelectedRows[0]; + var index = row.Index; // Removeしたら-1になるからその前に取得しておく + + // 行移動の実行 + ColumnDgv.Rows.Remove(row); + ColumnDgv.Rows.Insert(index + 1, row); + + // 行選択 + ColumnDgv.ClearSelection(); + ColumnDgv.Rows[index + 1].Selected = true; + } + + /// + /// DGVキー押下イベント + /// + /// + /// + private void ColumnDgv_KeyDown(object sender, KeyEventArgs e) + { + if (e.Alt && (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)) + { + if (e.KeyCode == Keys.Up) + { + UpButton_Click(sender, e); + } + else + { + DownButton_Click(sender, e); + } + + // 既定動作抑止(メニュー呼び/ビープを止める) + e.Handled = true; + e.SuppressKeyPress = true; + } + } + + private const string KEY_NAME_DISPLAY_INDEX_CSV = "DisplayIndexCsv"; + private const string KEY_NAME_COLUMN_VISIBLE_CSV = "ColumnVisibleCsv"; + private const string KEY_NAME_DISPLAY_NAME_CSV = "DisplayNameCsv"; + + /// + /// OKボタンクリック時 + /// + /// + /// + private void OkButton_Click(object sender, EventArgs e) + { + //List indexList = new List(); + //foreach (DataGridViewRow row in ColumnDgv.Rows) indexList.Add(((DataGridViewColumn)row.Tag).Index); + ////MessageBox.Show(string.Join(" ", indexList)); + //if (indexList.Max() != ColumnDgv.Rows.Count - 1) + if (_colNameToRowIndexDic.Values.Max() != ColumnDgv.Rows.Count - 1) + { + MessageBox.Show(this, $"_colNameToRowIndexDicがおかしい![_colNameToRowIndexDicのMax:{_colNameToRowIndexDic.Values.Max()},グリッド行数:{ColumnDgv.Rows.Count}]"); + this.DialogResult = DialogResult.None; + return; + } + + // 表示順 + int[] displayIndexArray = new int[ColumnDgv.Rows.Count]; + foreach (DataGridViewRow row in ColumnDgv.Rows) + displayIndexArray[_colNameToRowIndexDic[((DataGridViewRow)row).Cells[0].Value.ToString()]] + = ((DataGridViewRow)row).Index; + // INI書き込み + IniHelper.WriteValue(null, _targetDgv.Parent.Name + "." + _targetDgv.Name, KEY_NAME_DISPLAY_INDEX_CSV, string.Join(",", displayIndexArray)); + + // 表示非表示 + bool[] visibleArray = new bool[ColumnDgv.Rows.Count]; + foreach (DataGridViewRow row in ColumnDgv.Rows) + visibleArray[_colNameToRowIndexDic[((DataGridViewRow)row).Cells[0].Value.ToString()]] + = IsVisible(((DataGridViewRow)row).Index); + // INI書き込み + IniHelper.WriteValue(null, _targetDgv.Parent.Name + "." + _targetDgv.Name, KEY_NAME_COLUMN_VISIBLE_CSV, string.Join(",", visibleArray)); + + // 表示名称 + string[] nameArray = new string[ColumnDgv.Rows.Count]; + foreach (DataGridViewRow row in ColumnDgv.Rows) + { + var name = string.Empty; + if (row.Cells[COL_NAME_DISPLAY_NAME].Value.ToString() != row.Cells[COL_NAME_DEFAULT_DISPLAY_NAME].Value.ToString()) + { + name = row.Cells[COL_NAME_DISPLAY_NAME] + .Value + .ToString() + .Replace(",", "@@@COMMA@@@"); + } + nameArray[_colNameToRowIndexDic[((DataGridViewRow)row).Cells[0].Value.ToString()]] = name; + } + // INI書き込み + IniHelper.WriteValue(null, _targetDgv.Parent.Name + "." + _targetDgv.Name, KEY_NAME_DISPLAY_NAME_CSV, string.Join(",", nameArray)); + } + + /// + /// フォーム閉じるとき + /// + /// + /// + private void DgvColumnEditorForm_FormClosing(object sender, FormClosingEventArgs e) + { + // バウンズの保存 + FormHelper.SaveFormBounds(this); + + // DGV列幅保存 + DgvHelper.DgvSaveColWidth(ColumnDgv); + } + + /// + /// 表示名の編集メニュークリック + /// + /// + /// + private void EditDisplayNameMenu_Click(object sender, EventArgs e) + { + if (ColumnDgv.SelectedRows.Count < 1) return; + var rowIndex = ColumnDgv.SelectedRows[0].Index; + var text = ColumnDgv[COL_NAME_DISPLAY_NAME,rowIndex].Value.ToString(); + var defaultText= ColumnDgv[COL_NAME_DEFAULT_DISPLAY_NAME, rowIndex].Value.ToString(); + if (string.IsNullOrEmpty(text)) text = defaultText; + using (var form = new TextInputForm(text)) + { + if (form.ShowDialog() == DialogResult.Cancel) return; + + ColumnDgv[COL_NAME_DISPLAY_NAME, rowIndex].Value = form.EditedText == defaultText ? "" : form.EditedText; + } + } + + private void VisibleMenu_Click(object sender, EventArgs e) + { + if (ColumnDgv.SelectedRows.Count < 1) return; + var rowIndex = ColumnDgv.SelectedRows[0].Index; + SetVisible(rowIndex, !IsVisible(rowIndex)); + } + + private bool IsVisible(int rowIndex) + { + if (rowIndex < 0 || rowIndex >= ColumnDgv.Rows.Count) return true; + if (ColumnDgv.Rows[rowIndex].DefaultCellStyle == null) return true; + if (ColumnDgv.Rows[rowIndex].DefaultCellStyle.Font == null) return true; + var style = ColumnDgv.Rows[rowIndex].DefaultCellStyle.Font.Style; + return !((style & FontStyle.Strikeout) == FontStyle.Strikeout); + } + private void SetVisible(int rowIndex, bool visible) + { + if (rowIndex < 0 || rowIndex >= ColumnDgv.Rows.Count) return; + if (ColumnDgv.Rows[rowIndex].DefaultCellStyle == null) return; + Font font; + if (ColumnDgv.Rows[rowIndex].DefaultCellStyle.Font != null) + { + font = ColumnDgv.Rows[rowIndex].DefaultCellStyle.Font; + } + else + { + font = ColumnDgv.DefaultCellStyle.Font; + } + var style = font.Style; + if (visible) + { + style &= ~FontStyle.Strikeout; + } + else + { + style |= FontStyle.Strikeout; + } + ColumnDgv.Rows[rowIndex].DefaultCellStyle.Font = new Font(font, style); + + // 背景色 + ColumnDgv.Rows[rowIndex].DefaultCellStyle.BackColor = visible ? Color.Empty : Color.DarkGray; + } + } +} diff --git a/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.resx b/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.resx new file mode 100644 index 0000000..fb7e121 --- /dev/null +++ b/Forms/ContextMenuFunction/Gamen/DgvColumnEditorForm.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + True + + + True + + + 17, 17 + + \ No newline at end of file diff --git a/Forms/ContextMenuHelper.cs b/Forms/ContextMenuHelper.cs new file mode 100644 index 0000000..df99cb8 --- /dev/null +++ b/Forms/ContextMenuHelper.cs @@ -0,0 +1,332 @@ +using DocumentFormat.OpenXml.Office2010.CustomUI; +using KssSmaPlaLib.Forms.ContextMenuFunction; +using KssSmaPlaLib.Ini; +using KssSmaPlaLib.Input.Gamen; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Windows.UI.Popups; + +namespace KssSmaPlaLib.Forms +{ + public static class ContextMenuHelper + { + private const string INI_KEY_TITLE = "Title"; + private const string INI_KEY_BACK_COLOR = "BackColor"; + + /// + /// フォーム背景色選択 + /// + /// + public static void SelectBackColor(Form form) + { + using (ColorDialog cd = new ColorDialog()) + { + // 今の色を初期選択にしておく + cd.Color = form.BackColor; + + if (cd.ShowDialog() == DialogResult.OK) + { + // 1. フォームの背景色を即時変更 + form.BackColor = cd.Color; + + // 2. 🔥 Colorを16進数(#RRGGBB)に変換してINIに書き込み + string colorHex = ColorTranslator.ToHtml(cd.Color); + IniHelper.WriteValue(null, form.Name, INI_KEY_BACK_COLOR, colorHex); + } + } + } + + /// + /// 背景色をINIから取得して設定 + /// + /// + public static void SetBackColorFromIni(Form form) + { + var colorHex = IniHelper.ReadValue(null, form.Name, INI_KEY_BACK_COLOR, ColorTranslator.ToHtml(form.BackColor)); + form.BackColor = ColorTranslator.FromHtml(colorHex); + } + + /// + /// INIに登録されている色選択 + /// + /// + public static void SelectIniColor(Form form, string key, Color defaultColor) + { + var colorHex = IniHelper.ReadValue(null, form.Name, key, ColorTranslator.ToHtml(defaultColor)); + var color = ColorTranslator.FromHtml(colorHex); + + using (ColorDialog cd = new ColorDialog()) + { + // 今の色を初期選択にしておく + cd.Color = color; + + if (cd.ShowDialog() == DialogResult.OK) + { + // 2. 🔥 Colorを16進数(#RRGGBB)に変換してINIに書き込み + colorHex = ColorTranslator.ToHtml(cd.Color); + IniHelper.WriteValue(null, form.Name, key, colorHex); + } + } + } + + /// + /// INIに設定されている色取得 + /// + /// + /// + /// + /// + public static Color GetIniColor(Form form, string key, Color defaultColor) + { + var colorHex = IniHelper.ReadValue(null, form.Name, key, ColorTranslator.ToHtml(defaultColor)); + return ColorTranslator.FromHtml(colorHex); + } + + /// + /// チェックボックスのメニュー項目のチェック値のロード + /// + /// + /// + public static void MenuCheckBoxItemLoad(Form frm, ToolStripMenuItem menuItem) + { + menuItem.Checked = + //bool.TryParse( + // IniHelper.ReadValue( + // null, frm.Name, menuItem.Name, false.ToString()), + // out var boolVal) ? boolVal : false; + IniHelper.ReadBoolValue(null, frm.Name, menuItem.Name, false); + } + + public static void LoadAllMenuStates(Form form, ContextMenuStrip contextMenu) + { + foreach (ToolStripItem item in contextMenu.Items) + ProcessMenuItemLoad(form, item); + } + + private static void ProcessMenuItemLoad(Form form, ToolStripItem item) + { + // ToolStripMenuItem かつ CheckOnClick が true の場合のみ処理 + if (item is ToolStripMenuItem menuItem && menuItem.CheckOnClick) + { + // 🚀 ご提示のヘルパーメソッドを1行呼び出すだけ! + ContextMenuHelper.MenuCheckBoxItemLoad(form, menuItem); + } + + // 子メニュー(階層)がある場合は再帰的に奥まで処理 + if (item is ToolStripDropDownItem dropDownItem && dropDownItem.HasDropDownItems) + { + foreach (ToolStripItem subItem in dropDownItem.DropDownItems) + ProcessMenuItemLoad(form, subItem); + } + } + + /// + /// メニューのチェックボックスアイテムのクリック時 + /// + /// + /// + public static void MenuCheckBoxItemClick(Form frm, ToolStripMenuItem menuItem) + { + menuItem.Checked = !menuItem.Checked; + IniHelper.WriteValue(null, frm.Name, menuItem.Name, menuItem.Checked.ToString()); + } + + /// + /// メニューのフォント設定 + /// ※フォームロードでCallしてください + /// + /// + /// + public static void SetFont(Form form, ContextMenuStrip menu) + { + menu.Font = FontUxer.GetFont(form.Name, menu.Name + ".Font", menu.Font); + } + + /// + /// コントロール(ラベル等)のフォント設定 + /// ※フォームロードでCallしてください + /// + /// + /// + public static void SetControlFont(Form form, Control control) + { + control.Font = FontUxer.GetFont(form.Name, control.Name + ".Font", control.Font); + } + + /// + /// メニュー自身のフォント選択 + /// + /// + /// + public static void SelectMenuFont(Form form, ContextMenuStrip menu) + { + if (FontUxer.SelectFont(form, form.Name, menu.Name + ".Font", menu.Font, out var font)) + menu.Font = font; + } + + /// + /// コントロール(ラベル等)のフォント選択 + /// + /// + /// + public static void SelectControlFont(Form form, Control control) + { + if (FontUxer.SelectFont(form, form.Name, control.Name + ".Font", control.Font, out var font)) + control.Font = font; + } + + /// + /// フォームのタイトル設定 + /// + /// + public static void SetFormTitle(Form form) + { + var title = IniHelper.ReadValue(null, form.Name, INI_KEY_TITLE, form.Text); + using (var dlg = new TextInputForm(title)) + { + if (dlg.ShowDialog() == DialogResult.Cancel) return; + IniHelper.WriteValue(null, form.Name, INI_KEY_TITLE, dlg.EditedText); + form.Text = IniHelper.ReadValue(null, form.Name, INI_KEY_TITLE, form.Text); + } + } + + /// + /// バージョン表示 + /// + /// + public static void DispVersion(ToolStripMenuItem menuItem, bool isDispCraftedBy = true) + { + // バージョン + menuItem.Font = new Font(menuItem.Font.FontFamily, 8.0f); + menuItem.Enabled = false; + menuItem.Text = "Ver." + Application.ProductVersion + (isDispCraftedBy ? " | Crafted by のぼぼ工房" : ""); + } + + /// + /// ショートカットキーで動作するようにする + /// ※フォームのKeyPreviewをtrueにして、 + ///  フォームのKeyDownイベントでCallしてください。 + ///  private void Form1_KeyDown(object sender, KeyEventArgs e) + //  { + //   // メニューのEnabledを制御している場合、そのOpeningを呼んでおく。 + //   contextMenuStrip1_Opening(contextMenuStrip1, new System.ComponentModel.CancelEventArgs()); + // + //   if (ExecuteShortcutWithEnabledCheck(contextMenuStrip1.Items, e.KeyData)) + //   { + //   e.Handled = true; + //   } + //  } + /// + /// + /// + /// + public static bool ExecuteShortcutWithEnabledCheck(ToolStripItemCollection items, Keys keyData) + { + foreach (ToolStripItem item in items) + { + if (item is ToolStripMenuItem menuItem) + { + // ショートカットが一致し、かつ「有効(Enabled)」な時だけ実行! + if (menuItem.ShortcutKeys == keyData && menuItem.Enabled) + { + menuItem.PerformClick(); + return true; + } + + if (menuItem.HasDropDownItems) + { + if (ExecuteShortcutWithEnabledCheck(menuItem.DropDownItems, keyData)) return true; + } + } + } + return false; + } + + /// + /// コンボメニューの選択値をINIに保存 + /// (ToolStripComboBoxのSelectedIndexChangedでCallしてね!💖) + /// + /// + /// + public static void SaveComboMenuToIni(Form form, ToolStripComboBox combo) + { + IniHelper.WriteValue(null, form.Name, combo.Name + ".Values", combo.Text); + } + + /// + /// コンボメニューの選択値をINIから復元 + /// (FormのLoadでCallしてね💖) + /// + /// + /// + /// + public static void LoadComboMenuFromIni(Form form, ToolStripComboBox combo, string defaultValue) + { + combo.Text = IniHelper.ReadValue(null, form.Name, combo.Name + ".Values", defaultValue); + } + + /// + /// コンボメニューの選択値をINIから取得 + /// + /// + /// + /// + /// + public static string ComboMenuFromIni(Form form, ToolStripComboBox combo, string defaultValue) + { + return IniHelper.ReadValue(null, form.Name, combo.Name + ".Values", defaultValue); + } + + /// + /// フォームガチャ + /// + /// + /// + public static async Task FormGacha(Form form) + { + // 現在の状態をバックアップ + var originalState = form.Controls.Cast() + .Select(c => new { Ctrl = c, Loc = c.Location, Txt = c.Text, Font = c.Font }).ToList(); + var formLoc = form.Location; + + Random rng = new Random(); + + // 5秒間の狂宴(癒し) + for (int i = 0; i < 100; i++) + { + // 1. フォーム自体が震える + form.Location = new Point(formLoc.X + rng.Next(-20, 21), formLoc.Y + rng.Next(-20, 21)); + + foreach (var item in originalState) + { + // 2. コントロールがバラバラに踊る + item.Ctrl.Location = new Point(item.Loc.X + rng.Next(-15, 16), item.Loc.Y + rng.Next(-15, 16)); + + // 3. ラベルが叫び、ボタンが点滅する + if (item.Ctrl is System.Windows.Forms.Label) item.Ctrl.Text = "がちゃ!"; + item.Ctrl.BackColor = Color.FromArgb(rng.Next(256), rng.Next(256), rng.Next(256)); + } + + // 4. マウスカーソルを中央に拘束(逃がさない) + Cursor.Position = form.PointToScreen(new Point(form.Width / 2, form.Height / 2)); + + await Task.Delay(30); + } + + // 祭りのあと(完全復旧) + form.Location = formLoc; + foreach (var item in originalState) + { + item.Ctrl.Location = item.Loc; + item.Ctrl.Text = item.Txt; + item.Ctrl.Font = item.Font; + item.Ctrl.BackColor = SystemColors.Control; + } + } + } +} diff --git a/Forms/Controls/PoiDgv.cs b/Forms/Controls/PoiDgv.cs new file mode 100644 index 0000000..f894690 --- /dev/null +++ b/Forms/Controls/PoiDgv.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms.Controls +{ + public class PoiDgv : DataGridView + { + private IContainer components; + + [Category("Poi拡張"), Description("共通メニューを自動で追加するかどうか")] + public bool EnablePoiMenu { get; set; } = true; + + /// + /// コンストラクタ + /// + public PoiDgv() + { + // これが今回の本命! + this.DoubleBuffered = true; + + // DGB初期設定 + this.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; + this.RowHeadersVisible = false; + this.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + this.AllowUserToAddRows = false; + this.AllowUserToDeleteRows = false; + this.AllowUserToOrderColumns = true; + this.AllowUserToResizeColumns = true; + this.AllowUserToResizeRows = false; + this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.EnableResizing; + } + + /// + /// コンテキストメニュー設定時 + /// + /// + protected override void OnContextMenuStripChanged(EventArgs e) + { + base.OnContextMenuStripChanged(e); + + // ユーザーがデザイナーでセットしたメニューに「Poi」のスパイスを加える + if (EnablePoiMenu && this.ContextMenuStrip != null) + { + var menu = this.ContextMenuStrip; + + //// 重複防止チェック + //if (menu.Items.Find("poiItem_AutoFit", true).Length == 0) + //{ + // if (menu.Items.Count > 0) menu.Items.Add(new ToolStripSeparator()); + + // var fitItem = new ToolStripMenuItem("列幅をぽいっと自動調整"); + // fitItem.Name = "poiItem_AutoFit"; + // fitItem.Click += (s, ev) => this.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells); + + // menu.Items.Add(fitItem); + //} + } + } + + private void InitializeComponent() + { + ((System.ComponentModel.ISupportInitialize)(this)).BeginInit(); + this.SuspendLayout(); + // + // PoiDgv + // + this.RowTemplate.Height = 21; + ((System.ComponentModel.ISupportInitialize)(this)).EndInit(); + this.ResumeLayout(false); + + } + } +} + diff --git a/Forms/Controls/PoiDgv.resx b/Forms/Controls/PoiDgv.resx new file mode 100644 index 0000000..e5858cc --- /dev/null +++ b/Forms/Controls/PoiDgv.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + \ No newline at end of file diff --git a/Forms/Controls/PoiMainForm.cs b/Forms/Controls/PoiMainForm.cs new file mode 100644 index 0000000..abb2a04 --- /dev/null +++ b/Forms/Controls/PoiMainForm.cs @@ -0,0 +1,588 @@ +using DocumentFormat.OpenXml.VariantTypes; +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Windows.Media.Streaming.Adaptive; + +namespace KssSmaPlaLib.Forms.Controls +{ + /// + /// Poiメインフォーム + /// + public class PoiMainForm : Form + { + protected Label TitleLabel; + protected PoiDgv MainDgv; + protected ContextMenuStrip MainContextMenu; + protected ToolStripMenuItem TitleVisibleCheckMenu; + private ToolStripMenuItem _versionMenu; + private NumericUpDown _titleLabelYohakuHorizonNumeric = new NumericUpDown { Width = 60 }; + private NumericUpDown _titleLabelYohakuVarticalNumeric = new NumericUpDown { Width = 60 }; + // 👇遅延書き込みしようかと思ったがやめた! + //private Timer _saveDelayTimer = new Timer { Interval = 1000 }; + private string _title = string.Empty; + + /// + /// コンストラクタ + /// + public PoiMainForm(string title = "", string name = "") + { + // Name + this.Name = string.IsNullOrEmpty(name) ? this.GetType().Name : name; + + // タイトル + _title = string.IsNullOrEmpty(title) ? this.Name : title; + this.Text = _title; + + // フォームイベント + this.Load += PoiMainForm_Load; + this.FormClosing += PoiMainForm_FormClosing; + + // コントロールの生成と配置 + TitleLabel = new Label + { + Name = "TitleLabel", + AutoSize = true, + Dock = DockStyle.Top, + Text = "サンプルです💖", + }; + TitleLabel.Padding = new Padding(20, 5, 8, 20); + MainDgv = new PoiDgv + { + Name = "MainDgv", + Dock = DockStyle.Fill + }; + MainContextMenu = new ContextMenuStrip + { + Name = "MainContextMenu", + }; + this.ContextMenuStrip = MainContextMenu; + this.Controls.Add(MainDgv); + this.Controls.Add(TitleLabel); + + // デフォルトのメニュー項目登録 + CreateDefaultMenu(); + + DgvHelper.DgvLoadFontFromIni(MainDgv); + //ContextMenuHelper.MenuCheckBoxItemLoad(this, TitleVisibleCheckMenu); + TitleLabel.Visible = TitleVisibleCheckMenu.Checked; + ContextMenuHelper.SetControlFont(this, TitleLabel); + ContextMenuHelper.DispVersion(_versionMenu); + + // ソート無効 + foreach (DataGridViewColumn column in MainDgv.Columns) column.SortMode = DataGridViewColumnSortMode.NotSortable; + //// 日の列はセンタリング + //for (var day = 1; day <= 31; day++) MainDgv.Columns[$"ColDay{day}"].DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; + // TOTAL列は右寄せ + //MainDgv.Columns["ColTotal"].DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; + // ヘッダ行のデザインはOSのデザインを反映させる機能を解除 + MainDgv.EnableHeadersVisualStyles = false; + + // 👇やろうかと思ったけどやめた! + //// 遅延書き込みタイマー + //_saveDelayTimer.Tick += (s, e) => + //{ + // _saveDelayTimer.Stop(); // タイマーを止める + // // 👑 ここでINIファイルへの書き込みを実行! + // // スピンボタン連打が「止まってから1秒後」に1回だけ綺麗に保存されます。 + // aaaaaaa + //}; + //LoadDataAsync(); + + // タイトル文字列設定 + //TitleLabel.Text = GetTitle(); + } + + /// + /// フォームロード時 + /// + /// + /// + /// + private void PoiMainForm_Load(object sender, EventArgs e) + { + FormHelper.LoadFormBounds(this); + DgvHelper.DgvLoadFontFromIni(MainDgv); + DgvHelper.DgvLoadColWidth(MainDgv); + DgvHelper.DgvLoadHeaderHeight(MainDgv); + DgvHelper.DgvLoadColVisible(MainDgv); + DgvHelper.DgvLoadColDisplayIndex(MainDgv); + DgvHelper.DgvLoadColFrozen(MainDgv); + DgvHelper.DgvSetHeaderBackColorFromIni(this, MainDgv); + //DgvHelper.DgvSetHeaderBackColorFromIniWithNameMatch(this, MainDgv, "ColDay*", INI_KEY_HEADER_DAY_COLUMN_BACK_COLOR); + + // 👇これはLoadでやらないと、子で作ってるメニューアイテムに反映されない! + ContextMenuHelper.LoadAllMenuStates(this, MainContextMenu); + ContextMenuHelper.SetFont(this, MainContextMenu); + var vertical = + int.TryParse( + IniHelper.ReadValue(null, this.Name, _titleLabelYohakuVarticalNumeric.Name, "0"), + out var intVal) ? intVal : 0; + var horizon = + int.TryParse( + IniHelper.ReadValue(null, this.Name, _titleLabelYohakuHorizonNumeric.Name, "0"), + out intVal) ? intVal : 0; + TitleLabel.Padding = new Padding(horizon, vertical, horizon, vertical); + } + + /// + /// フォーム閉じるとき + /// + /// + /// + /// + private void PoiMainForm_FormClosing(object sender, FormClosingEventArgs e) + { + FormHelper.SaveFormBounds(this); + DgvHelper.DgvSaveFontToIni(MainDgv); + DgvHelper.DgvSaveColWidth(MainDgv); + DgvHelper.DgvSaveHeaderHeight(MainDgv); + DgvHelper.DgvSaveColVisible(MainDgv); + DgvHelper.DgvSaveColDisplayIndex(MainDgv); + DgvHelper.DgvSaveColFrozen(MainDgv); + IniHelper.WriteValue( + null, this.Name, + _titleLabelYohakuHorizonNumeric.Name, + _titleLabelYohakuHorizonNumeric.Value.ToString()); + IniHelper.WriteValue( + null, this.Name, + _titleLabelYohakuVarticalNumeric.Name, + _titleLabelYohakuVarticalNumeric.Value.ToString()); + } + + /// + /// デフォルトのメニュー項目(表示設定など)を作成 + /// + private void CreateDefaultMenu() + { + // 仕切り線 + MainContextMenu.Items.Add(new ToolStripSeparator()); + + // 表示設定(親) + var hyojiSetteiMenu = new ToolStripMenuItem() + { + Name = "HyojiSetteiMenu", + Text = "表示設定", + }; + MainContextMenu.Items.Add(hyojiSetteiMenu); + + // メニューのフォント(クリック動作) + var menuFontMenu = new ToolStripMenuItem() + { + Name = "MenuFontMenu", + Text = "メニューのフォント...", + }; + menuFontMenu.Click += (s, e)=> + { + ContextMenuHelper.SelectMenuFont(this, MainContextMenu); + }; + hyojiSetteiMenu.DropDownItems.Add(menuFontMenu); + + // グリッドメニュー(親) + var gridMenu = new ToolStripMenuItem() + { + Name = "GridMenu", + Text = "グリッド", + }; + hyojiSetteiMenu.DropDownItems.Add(gridMenu); + + // グリッドフォントメニュー(クリック動作) + var gridFontMenu = new ToolStripMenuItem() + { + Name = "GridFontMenu", + Text = "フォント...", + }; + gridFontMenu.Click += (s, e) => + { + DgvHelper.DgvSelectFont(this, MainDgv); + }; + gridMenu.DropDownItems.Add(gridFontMenu); + + // グリッド列非表示メニュー(クリック動作) + var gridColumnDisabledMenu = new ToolStripMenuItem() + { + Name = "GridColumnDisabledMenu", + Text = "この列を非表示にする", + }; + gridColumnDisabledMenu.Click += (s, e) => + { + if (MainDgv.CurrentCell is null) return; + MainDgv.Columns[MainDgv.CurrentCell.ColumnIndex].Visible = false; + }; + gridMenu.DropDownItems.Add(gridColumnDisabledMenu); + + // グリッド列再表示メニュー(クリック動作) + var gridColumnRedispMenu = new ToolStripMenuItem() + { + Name = "GridColumnRedispMenu", + Text = "列の再表示", + }; + gridColumnRedispMenu.Click += (s, e) => + { + DgvHelper.DgvRedispAllColumn(MainDgv); + }; + gridMenu.DropDownItems.Add(gridColumnRedispMenu); + + // グリッド列幅自動調整メニュー(クリック動作) + var columnWidthAutoAdjustMenu = new ToolStripMenuItem() + { + Name = "ColumnWidthAutoAdjustMenu", + Text = "列幅の自動調整", + }; + columnWidthAutoAdjustMenu.Click += (s, e) => + { + DgvHelper.DgvColumnWidthAutoAdjust(MainDgv); + }; + gridMenu.DropDownItems.Add(columnWidthAutoAdjustMenu); + + // グリッド列固定メニュー(クリック動作) + var gridColumnFixMenu = new ToolStripMenuItem() + { + Name = "GridColumnFixMenu", + Text = "列固定", + }; + gridColumnFixMenu.Click += (s, e) => + { + DgvHelper.DgvColumnFix(MainDgv); + }; + gridMenu.DropDownItems.Add(gridColumnFixMenu); + + // グリッド列固定解除メニュー(クリック動作) + var gridColumnFixKaijoMenu = new ToolStripMenuItem() + { + Name = "GridColumnFixKaijoMenu", + Text = "列固定解除", + }; + gridColumnFixKaijoMenu.Click += (s, e) => + { + DgvHelper.DgvColumnFixKaijo(MainDgv); + }; + gridMenu.DropDownItems.Add(gridColumnFixKaijoMenu); + + // グリッドのヘッダ(親) + //this.GridHeaderMenu}); + var gridHeaderMenu = new ToolStripMenuItem() + { + Name = "GridHeaderMenu", + Text = "ヘッダ", + }; + gridMenu.DropDownItems.Add(gridHeaderMenu); + + // グリッドヘッダ背景色(クリック動作) + var gridHeaderBackColorMenu = new ToolStripMenuItem() + { + Name = "GridHeaderBackColorMenu", + Text = "ヘッダ背景色...", + }; + gridHeaderBackColorMenu.Click += (s, e) => + { + DgvHelper.DgvSelectHeaderBackColor(this, MainDgv); + }; + gridHeaderMenu.DropDownItems.Add(gridHeaderBackColorMenu); + + // グリッドヘッダの日の背景色(クリック動作) + var gridHeaderDayBackColorMenu = new ToolStripMenuItem() + { + Name = "GridHeaderDayBackColorMenu", + Text = "日の背景色...", + Enabled = false, // デフォルトは非活性 + }; + gridHeaderDayBackColorMenu.Click += (s, e) => + { + DgvHelper.DgvSelectHeaderBackColorWithNameMatch(this, MainDgv, "ColDay*", gridHeaderDayBackColorMenu.Name); + }; + gridHeaderMenu.DropDownItems.Add(gridHeaderDayBackColorMenu); + + // グリッドヘッダの非稼働日の背景色(クリック動作) + var gridHeaderHikadobiBackColorMenu = new ToolStripMenuItem() + { + Name = "GridHeaderHikadobiBackColorMenu", + Text = "非稼働日の背景色...", + Enabled = false, // デフォルトは非活性 + }; + gridHeaderHikadobiBackColorMenu.Click += (s, e) => + { + // Comming soon! + MessageBox.Show(this,"カミングスーン!",this.Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + }; + gridHeaderMenu.DropDownItems.Add(gridHeaderHikadobiBackColorMenu); + + //this.GridHeaderMenu.DropDownItems.AddRange( + // new System.Windows.Forms.ToolStripItem[] { + //this., + //this., + //this.}); + + // グリッドメニューを開くときの制御 + gridMenu.DropDownOpening += (s, e) => + { + // 「この列の非表示」はアクティブセルがあるときだけ有効 + gridColumnDisabledMenu.Enabled = !(MainDgv.CurrentCell is null); + // 「列の再表示」は非表示列があるときだけ有効 + gridColumnRedispMenu.Enabled = + MainDgv.Columns.GetColumnCount(DataGridViewElementStates.Visible) + != MainDgv.Columns.Count; + // 「列固定」はアクティブセルがあるときだけ有効 + gridColumnFixMenu.Enabled = !(MainDgv.CurrentCell is null); + // 「列固定解除」は列固定がされているときだけ有効 + gridColumnFixKaijoMenu.Enabled = MainDgv.Columns + .Cast() + .Any(c => c.Frozen); + }; + + // タイトル(親) + var titleMenu = + new ToolStripMenuItem() + { + Name = "TitleMenu", + Text = "タイトル", + }; + hyojiSetteiMenu.DropDownItems.Add(titleMenu); + + // タイトル表示(チェックボックス) + TitleVisibleCheckMenu = + new ToolStripMenuItem() + { + Name = "TitleVisibleCheckMenu", + Text = "タイトル表示?", + CheckOnClick = true, + }; + TitleVisibleCheckMenu.CheckedChanged += (s, e) => + { + var mi = (ToolStripMenuItem)s; + IniHelper.WriteBoolValue(null, this.Name, mi.Name, + mi.Checked); + TitleLabel.Visible = mi.Checked; + }; + titleMenu.DropDownItems.Add(TitleVisibleCheckMenu); + + // タイトルフォントメニュー(クリック動作) + var titleFontMenu = + new ToolStripMenuItem() + { + Name = "TitleFontMenu", + Text = "フォント...", + }; + titleFontMenu.Click += (s, e) => + { + ContextMenuHelper.SelectControlFont(this, TitleLabel); + }; + titleMenu.DropDownItems.Add(titleFontMenu); + + // タイトル余白(親) + var titleYohakuMenu = new ToolStripMenuItem() + { + Name = "TitleYohakuMenu", + Text = "余白", + }; + titleMenu.DropDownItems.Add(titleYohakuMenu); + + // 余白設定パネル + titleYohakuMenu.DropDownItems.Add(CreateYohakuPanelMenu()); + + // 仕切り線 + MainContextMenu.Items.Add(new ToolStripSeparator()); + + // バージョン表示メニュー(表示のみ) + _versionMenu = + new ToolStripMenuItem() + { + Name = "VersionMenu", + Text = "バージョン情報", + }; + MainContextMenu.Items.Add(_versionMenu); + + //titleMenu.DropDownItems.Add(CreateYohakuPanelMenu()); + //MainContextMenu.Items.Add(CreateYohakuPanelMenu()); + + // 実験!✨✨✨ + + //// NumericUpDownを生成して設定 + //NumericUpDown numericUpDown = new NumericUpDown(); + //numericUpDown.Minimum = 0; + //numericUpDown.Maximum = 100; + //numericUpDown.Value = 10; + //numericUpDown.Width = 60; // メニュー内で邪魔にならない幅に調整 + //// ToolStripControlHostでラップしてメニューに追加 + //ToolStripControlHost host = new ToolStripControlHost(numericUpDown); + //MainContextMenu.Items.Add(host); // または menuStrip1.Items.Add(host); + } + + /// + /// 余白調整パネルメニュー作成 + /// + /// + private ToolStripControlHost CreateYohakuPanelMenu() + { + // 1. 土台となるパネル(縦方向にコントロールを並べる) + TableLayoutPanel miniDialog = new TableLayoutPanel(); + miniDialog.ColumnCount = 2; // 2列のグリッド + miniDialog.RowCount = 2; // 3; // 3行 + miniDialog.AutoSize = true; + miniDialog.Padding = new Padding(10); // 周囲に余白を作ってダイアログ感を出す + miniDialog.BackColor = Color.FromArgb(240, 240, 240); // ほんのり背景色を変える + // 2. コントロール達を作成 + Label lblX = new Label { Text = "横余白:", AutoSize = true, Anchor = AnchorStyles.Left }; + _titleLabelYohakuHorizonNumeric = new NumericUpDown + { + Name = "TitleLabelYohakuHorizonNumeric", + Width = 60, + TextAlign = HorizontalAlignment.Right, + }; + _titleLabelYohakuHorizonNumeric.ValueChanged += (sender, e) => + { + TitleLabel.Padding = new Padding( + (int)_titleLabelYohakuHorizonNumeric.Value, + TitleLabel.Padding.Top, + (int)_titleLabelYohakuHorizonNumeric.Value, + TitleLabel.Padding.Bottom); + // 🔥🌊INI書きはFormClosingで行う! + }; + Label lblY = new Label { Text = "縦余白:", AutoSize = true, Anchor = AnchorStyles.Left }; + _titleLabelYohakuVarticalNumeric = new NumericUpDown + { + Name = "TitleLabelYohakuVarticalNumeric", + Width = 60, + TextAlign = HorizontalAlignment.Right, + }; + _titleLabelYohakuVarticalNumeric.ValueChanged += (sender, e) => + { + TitleLabel.Padding = new Padding( + TitleLabel.Padding.Left, + (int)_titleLabelYohakuVarticalNumeric.Value, + TitleLabel.Padding.Right, + (int)_titleLabelYohakuVarticalNumeric.Value); + // 🔥🌊INI書きはFormClosingで行う! + }; + //CheckBox chkAuto = new CheckBox { Text = "自動調整", AutoSize = true }; + // 3. グリッドパネルに綺麗に配置 + miniDialog.Controls.Add(lblX, 0, 0); + miniDialog.Controls.Add(_titleLabelYohakuHorizonNumeric, 1, 0); + miniDialog.Controls.Add(lblY, 0, 1); + miniDialog.Controls.Add(_titleLabelYohakuVarticalNumeric, 1, 1); + //// チェックボックスは2列分ぶち抜きで配置 + //miniDialog.Controls.Add(chkAuto, 0, 2); + //miniDialog.SetColumnSpan(chkAuto, 2); + // 4. メニューにホストする + //ToolStripControlHost dialogHost = new ToolStripControlHost(miniDialog); + //MainContextMenu.Items.Add(dialogHost); + + // 開くときの処理 + var tssh = new ToolStripControlHost(miniDialog); + tssh.OwnerChanged += (sender, e) => + { + // 親となるメニュー(ToolStripDropDown)を取得 + if (tssh.Owner is ToolStripDropDown parentMenu) + { + // 親の「開く直前イベント」に、自分のメソッドをこっそり登録する! + parentMenu.Opening -= ParentMenu_Opening; // 二重登録防止 + parentMenu.Opening += ParentMenu_Opening; + } + }; + return tssh; + } + + /// + /// 親メニュー開くときに追加する処理 + /// + /// + /// + private void ParentMenu_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + //// フォントを親に合わせる(階層深いと反映されないため) + //_titleLabelYohakuHorizonNumeric.Font = + // _titleLabelYohakuVarticalNumeric.Font = + // MainContextMenu.Font; + // パネルの合わせ、再レイアウト(パネルが階層奥深くだと、なぜか反映されていなかったりする) + var parent = _titleLabelYohakuHorizonNumeric.Parent; + if (parent != null) + { + parent.Font = MainContextMenu.Font; + parent.PerformLayout(); + } + + // タイトルラベルの現在の余白をNumericUpDownに設定 + _titleLabelYohakuHorizonNumeric.Value = TitleLabel.Padding.Left; + _titleLabelYohakuVarticalNumeric.Value = TitleLabel.Padding.Top; + } + + /// + /// メニュー項目追加 + /// + /// + /// + /// + /// + /// + protected ToolStripMenuItem AddCheckMenuItem( + int index, + string text, + string name, + bool defaultValue, + Action onCheckedChanged = null) + { + var item = new ToolStripMenuItem(text); + + // チェックボックスとして機能する + item.CheckOnClick = true; + + item.Name = name; + + // 3. CheckedChanged イベントをここで一括で仕込む! + item.CheckedChanged += (sender, e) => + { + var mi = (ToolStripMenuItem)sender; + + // チェック値をINI保存 + IniHelper.WriteBoolValue(null, this.Name, mi.Name, mi.Checked); + + // イベントプロシージャ(あるとき) + onCheckedChanged?.Invoke(mi.Checked); + }; + + MainContextMenu.Items.Insert(index, item); + return item; + } + + public enum PoiAlign + { + Left, + Right, + Center, + } + + private DataGridViewContentAlignment GetDgvAlign(PoiAlign align) + { + switch (align) + { + case PoiAlign.Left: return DataGridViewContentAlignment.MiddleLeft; + case PoiAlign.Center: return DataGridViewContentAlignment.MiddleCenter; + case PoiAlign.Right: return DataGridViewContentAlignment.MiddleRight; + default: return DataGridViewContentAlignment.MiddleLeft; + } + } + + /// + /// 列追加 + /// + /// + /// + /// + protected void AddColumn(string name, string text, PoiAlign align = PoiAlign.Left) + { + var col = new DataGridViewTextBoxColumn() + { + Name = name, + HeaderText = text, + ReadOnly = true, + }; + col.DefaultCellStyle.Alignment = GetDgvAlign(align); + MainDgv.Columns.Add(col); + } + } +} diff --git a/Forms/DataGridViewExporter.cs b/Forms/DataGridViewExporter.cs new file mode 100644 index 0000000..1615c94 --- /dev/null +++ b/Forms/DataGridViewExporter.cs @@ -0,0 +1,172 @@ +using ClosedXML.Excel; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms +{ + public static class DataGridViewExporter + { + + /// + /// DataGridView の「可視列・可視行のみ」を Excel へエクスポートします。 + /// 列順は DisplayIndex 順/ヘッダー付き/自動調整/簡易罫線付き。 + /// + public static void ExportVisibleToExcel( + DataGridView dgv, + string filePath, + string sheetName = "Export") + { + if (dgv == null) throw new ArgumentNullException(nameof(dgv)); + if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentException("filePath is required.", nameof(filePath)); + + // 可視列のみ、DisplayIndex順に並べ替え + var visibleCols = dgv.Columns + .Cast() + .Where(c => c.Visible) + .OrderBy(c => c.DisplayIndex) + .ToList(); + + // 可視行のみ(新規行除外) + var visibleRows = dgv.Rows + .Cast() + .Where(r => r.Visible && !r.IsNewRow) + .ToList(); + + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add(string.IsNullOrWhiteSpace(sheetName) ? "Sheet1" : sheetName); + + int row = 1; + int col = 1; + + // ヘッダー行 + foreach (var c in visibleCols) + { + ws.Cell(row, col).Value = c.HeaderText ?? c.Name; + // すこし見やすく + ws.Cell(row, col).Style.Font.Bold = true; + ws.Cell(row, col).Style.Fill.BackgroundColor = XLColor.LightGray; + ws.Cell(row, col).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + col++; + } + + // データ行 + row = 2; + foreach (var r in visibleRows) + { + col = 1; + foreach (var c in visibleCols) + { + var cell = r.Cells[c.Index]; + var val = cell.Value; + + if (val == null || val == DBNull.Value) + { + ws.Cell(row, col).Clear(); // 空セル(文字列""よりこちらが自然) + } + else + { + // ValueType を優先し、なければ実オブジェクトの型で判定 + var vt = cell.ValueType ?? val.GetType(); + + if (vt == typeof(int) || vt == typeof(long) || vt == typeof(short) || vt == typeof(byte)) + { + ws.Cell(row, col).Value = Convert.ToInt64(val); // 長整数に寄せると安全 + // 必要なら桁区切りなど: ws.Cell(row, col).Style.NumberFormat.Format = "#,##0"; + } + else if (vt == typeof(decimal)) + { + ws.Cell(row, col).Value = Convert.ToDecimal(val); + // 書式例: ws.Cell(row, col).Style.NumberFormat.Format = "#,##0.####"; + } + else if (vt == typeof(double) || vt == typeof(float)) + { + ws.Cell(row, col).Value = Convert.ToDouble(val); + } + else if (vt == typeof(DateTime)) + { + ws.Cell(row, col).Value = Convert.ToDateTime(val); + // 例: ws.Cell(row, col).Style.DateFormat.Format = "yyyy-mm-dd hh:mm"; + } + else if (vt == typeof(TimeSpan)) + { + // Excelは TimeSpan を日数として扱うので適宜書式を + var ts = (TimeSpan)val; + ws.Cell(row, col).Value = ts; + ws.Cell(row, col).Style.NumberFormat.Format = "[h]:mm:ss"; + } + else if (vt == typeof(bool)) + { + ws.Cell(row, col).Value = Convert.ToBoolean(val); + } + else + { + // それ以外は文字列にフォールバック(トリム等はお好みで) + ws.Cell(row, col).Value = val.ToString(); + } + } + col++; + } + row++; + } + + //// データ行 + //row = 2; + //foreach (var r in visibleRows) + //{ + // col = 1; + // foreach (var c in visibleCols) + // { + // object val = r.Cells[c.Index].Value; + // // Excel側でよく見えるように値をそのままセット + // ws.Cell(row, col).Value = val == null ? string.Empty : val.ToString(); + // col++; + // } + // row++; + //} + + // テーブルっぽく罫線&自動調整 + var lastRow = row - 1; + var lastCol = visibleCols.Count; + if (lastCol > 0 && lastRow > 0) + { + var rng = ws.Range(1, 1, lastRow, lastCol); + rng.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + rng.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + rng.SetAutoFilter(); // フィルターもあると便利 + ws.Columns(1, lastCol).AdjustToContents(); + } + + // 保存 + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + wb.SaveAs(filePath); + } + } + + /// + /// SaveFileDialog 付きの簡便版。UIからサクッと呼び出す用。 + /// + public static void ExportVisibleToExcelWithDialog(DataGridView dgv, string defaultFileName = "export.xlsx") + { + using (var sfd = new SaveFileDialog() + { + Filter = "Excel Workbook (*.xlsx)|*.xlsx", + FileName = defaultFileName, + OverwritePrompt = true, + AddExtension = true + }) + { + if (sfd.ShowDialog() == DialogResult.OK) + { + ExportVisibleToExcel(dgv, sfd.FileName); + MessageBox.Show("Excelにエクスポートしました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + } + } +} diff --git a/Forms/DatePickerHelper.cs b/Forms/DatePickerHelper.cs new file mode 100644 index 0000000..d432353 --- /dev/null +++ b/Forms/DatePickerHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms +{ + public static class DatePickerHelper + { + public static DateTime? ShowDateDialog( + DateTime? defaultDate = null, + string title = "日付を選択してください") + { + // 1. その場限りのフォームを作成 + using (Form form = new Form()) + { + DateTimePicker dtp = new DateTimePicker(); + Button btnOk = new Button(); + + // フォームの設定 + form.Text = title; + form.Size = new System.Drawing.Size(240, 120); + form.FormBorderStyle = FormBorderStyle.FixedDialog; + form.StartPosition = FormStartPosition.CenterParent; + form.MaximizeBox = false; + form.MinimizeBox = false; + + // DateTimePickerの設定 + dtp.Location = new System.Drawing.Point(10, 10); + dtp.Size = new System.Drawing.Size(200, 20); + dtp.Format = DateTimePickerFormat.Short; + + // ★ ここで初期値をセット! + // 引数がnullなら今日の値を、あればその値をセットします。 + dtp.Value = defaultDate ?? DateTime.Today; + + // OKボタンの設定 + btnOk.Text = "OK"; + btnOk.DialogResult = DialogResult.OK; // これを押すとフォームが閉じる + btnOk.Location = new System.Drawing.Point(135, 45); + form.AcceptButton = btnOk; // EnterキーでOK + + // コントロールを追加 + form.Controls.Add(dtp); + form.Controls.Add(btnOk); + + // 2. ダイアログとして表示 + if (form.ShowDialog() == DialogResult.OK) + { + return dtp.Value; // 選択された日付を返す + } + } + return null; // キャンセルされた場合 + } + } +} diff --git a/Forms/DgvHelper.cs b/Forms/DgvHelper.cs new file mode 100644 index 0000000..2221eb1 --- /dev/null +++ b/Forms/DgvHelper.cs @@ -0,0 +1,726 @@ +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Forms.ContextMenuFunction; +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms +{ + public class DgvHelper + { + /// + /// 汎用的な初期化 + /// + /// + public static void Initialize(DataGridView dgv) + { + // 行選択 + dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + + // 行ヘッダ消去 + dgv.RowHeadersVisible = false; + + // 編集不可 + dgv.ReadOnly = true; + + // ユーザ操作制限 + dgv.AllowUserToAddRows = false; + dgv.AllowUserToDeleteRows = false; + dgv.AllowUserToOrderColumns = false; + dgv.AllowUserToResizeColumns = true; + dgv.AllowUserToResizeRows = false; + + // 列ヘッダはセンタリング + dgv.ColumnHeadersDefaultCellStyle.Alignment=DataGridViewContentAlignment.MiddleCenter; + + //// ソート用余白を消す + //dgv.ColumnHeadersDefaultCellStyle.Padding = new Padding(0); + + // セル内余白(上下左右)をゼロに + dgv.DefaultCellStyle.Padding = new Padding(0); + + // 行高さの自動調整 + dgv.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells; + + // ダブルバッファ(プロパティが隠れている) + var type = typeof(DataGridView); + var prop = type.GetProperty("DoubleBuffered", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + prop.SetValue(dgv, true, null); + } + + /// + /// 列ヘッダのソート用余白を消す + /// ※列が確定してからCallすること + /// + /// + //public static void ClearColumnHeaderSortYohaku(DataGridView dgv) + //{ + // // 列ヘッダの幅にソート用の幅は不要 + // //foreach (DataGridViewColumn col in dgv.Columns) col.SortMode = DataGridViewColumnSortMode.NotSortable; + + // // 何をやってもダメなので、テキストの先頭に余白入れます!👉ダメ!orz + // //const string YOHAKU = "       "; + // //foreach (DataGridViewColumn col in dgv.Columns) col.HeaderText = YOHAKU + col.HeaderText; + //} + + // どちらのDTOでも使える共通バインド関数 + public static void BindListWithAttributeHeaders( + DataGridView grid, + IList source, + bool isForce = false) + { + // 強制でないとき、データソースありなら何もしない + if (!(grid.DataSource is null) && !isForce) return; + + // データバインド完了後にヘッダー等を属性で上書き + grid.DataBindingComplete += (_, __) => ApplyAttributeHeaders(grid); + + // 「とりあえず ToString() で文字列表示」したい場合の統一フォーマット + grid.CellFormatting += (_, e) => + { + try + { + // AutoGenerateColumnsだと各セルの Value はプロパティ値になります + // ここで ToString() 統一(nullは空文字に) + if (e.Value == null) { e.Value = string.Empty; e.FormattingApplied = true; return; } + // 文化依存や日付/数値の既定書式を崩したくなければコメントアウト推奨 + if (!(e.Value is bool) && + string.IsNullOrEmpty(grid.Columns[e.ColumnIndex].DefaultCellStyle.Format)) + { + e.Value = e.Value.ToString(); + e.FormattingApplied = true; + } + + } + catch (Exception ex) + { + MessageBox.Show(ex.Message); + } + }; + + grid.AutoGenerateColumns = true; + + // 変更通知を効かせたい場合は BindingList に包む + var bindingList = source is BindingList bl ? bl : new BindingList(source); + + // BindingSource 経由が安定 + var bs = new BindingSource { DataSource = bindingList }; + grid.DataSource = bs; + } + + private const string KEY_NAME_DISPLAY_NAME_CSV = "DisplayNameCsv"; + + /// + /// プライベートな子関数 + /// + /// + /// + public static void ApplyAttributeHeaders(DataGridView grid) + { + // 編集した表示名 + var csv = IniHelper.ReadValue(null, grid.Parent.Name + "." + grid.Name, KEY_NAME_DISPLAY_NAME_CSV); + + var props = TypeDescriptor.GetProperties(typeof(T)) + .Cast() + .ToDictionary(p => p.Name); + + foreach (DataGridViewColumn col in grid.Columns) + { + var key = col.DataPropertyName ?? col.Name; + if (key is null) continue; + + if (props.TryGetValue(key, out var pd)) + { + // タグに入れておく(PropertyDescriptor型) + col.Tag = pd; + + // 列ヘッダーを [DisplayName] に + col.HeaderText = pd.DisplayName; + + // 表示名を登録されてたら、置き換える + var editName = StringUxer.GetStringByIndexFormCsv(csv,col.Index,string.Empty); + if (!string.IsNullOrEmpty(editName)) col.HeaderText = editName; + + // 文字寄せ + var alignment = + (col.HeaderText.IndexOf("[R]") >= 0 ? DataGridViewContentAlignment.MiddleRight : + (col.HeaderText.IndexOf("[C]") >= 0 ? DataGridViewContentAlignment.MiddleCenter : + DataGridViewContentAlignment.MiddleLeft)); + col.DefaultCellStyle.Alignment = alignment; + col.HeaderText = col.HeaderText.Replace("[L]", "").Replace("[C]", "").Replace("[R]", ""); + + // [Browsable(false)] の列は非表示(AutoGenerateでも列が立つ場合の保険) + var browsableAttr = pd.Attributes.OfType().FirstOrDefault(); + if (browsableAttr != null && !browsableAttr.Browsable) + { + col.Visible = false; + continue; + } + + // 読み取り専用属性の尊重 + col.ReadOnly = pd.IsReadOnly; + + // 便利系:ソートを自動に + // col.SortMode = DataGridViewColumnSortMode.Automatic; + + // 必要なら列幅の初期値 + // col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells; + } + } + } + + private const string INI_KEY_COL_WIDTH_CSV = "ColWidthCsv"; + + /// + /// 列幅保存(FormのFormClosingにてCall) + /// + /// + public static void DgvSaveColWidth(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = new List(); + foreach(DataGridViewColumn col in dgv.Columns) list.Add(col.Width); + IniHelper.WriteValue(null, section, INI_KEY_COL_WIDTH_CSV, string.Join(",", list)); + } + + /// + /// 列幅復元(FormのコンストラクタにてCall) + /// + /// + public static void DgvLoadColWidth(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = IniHelper.ReadValue(null, section,INI_KEY_COL_WIDTH_CSV).Split(',').ToList(); + foreach (DataGridViewColumn col in dgv.Columns) + { + if (list.Count - 1 < col.Index) break; + if (!int.TryParse(list[col.Index], out int val)) break; + col.Width = val; + } + } + + private const string INI_KEY_FONT_CSV = "Font"; + + public static void DgvSelectFont(Form form, DataGridView dgv) + { + if (!FontUxer.SelectFont(form, form.Name, dgv.Name + ".Font", dgv.DefaultCellStyle.Font, out var font)) return; + DgvSetFontFromFont(dgv, font); + DgvSaveFontToIni(dgv); + } + + + public static void DgvSetFontFromFont(DataGridView dgv, Font font) + { + var font2 = new Font(font.Name, font.Size, font.Bold ? FontStyle.Bold : FontStyle.Regular); + dgv.DefaultCellStyle.Font = font2; + dgv.ColumnHeadersDefaultCellStyle.Font = font2; + dgv.AutoResizeRows(); + } + + public static void DgvSaveFontToIni(DataGridView dgv) + { + var fontCsv = + dgv.ColumnHeadersDefaultCellStyle.Font.Name + "," + + dgv.ColumnHeadersDefaultCellStyle.Font.Size.ToString() + "," + + dgv.ColumnHeadersDefaultCellStyle.Font.Bold.ToString(); + IniHelper.WriteValue(null, dgv.Parent.Name + "." + dgv.Name, INI_KEY_FONT_CSV, fontCsv); + } + + public static void DgvLoadFontFromIni(DataGridView dgv) + { + var defaultFontCsv = + dgv.ColumnHeadersDefaultCellStyle.Font.Name + "," + + dgv.ColumnHeadersDefaultCellStyle.Font.Size.ToString() + "," + + dgv.ColumnHeadersDefaultCellStyle.Font.Bold.ToString(); + var iniFontCsv = IniHelper.ReadValue(null, dgv.Parent.Name + "." + dgv.Name, INI_KEY_FONT_CSV, defaultFontCsv); + var fontItems = iniFontCsv.Split(','); + if (fontItems.Length != 3) fontItems = defaultFontCsv.Split(','); + if (float.TryParse(fontItems[1], out var size) == false) size = dgv.ColumnHeadersDefaultCellStyle.Font.Size; + if (bool.TryParse(fontItems[2], out var bold) == false) bold = dgv.ColumnHeadersDefaultCellStyle.Font.Bold; + var font = new Font(fontItems[0], size, bold ? FontStyle.Bold : FontStyle.Regular); + dgv.DefaultCellStyle.Font = font; + dgv.ColumnHeadersDefaultCellStyle.Font = font; + dgv.AutoResizeRows(); + } + + private const string KEY_NAME_DISPLAY_INDEX_CSV = "DisplayIndexCsv"; + private const string KEY_NAME_COLUMN_VISIBLE_CSV = "ColumnVisibleCsv"; + + /// + /// INIの列表示順をDGVに反映 + /// + /// + public static void SetColumnDisplayIndexFromIni(DataGridView dgv) + { + // 表示順(DisplayIndex)の設定 + var csv = IniHelper.ReadValue(null, dgv.Parent.Name + "." + dgv.Name, KEY_NAME_DISPLAY_INDEX_CSV); + if (string.IsNullOrEmpty(csv)) return; + var items = csv.Split(','); + if (items.Length != dgv.Columns.Count) return; + List displayIndexList = new List(); + foreach ( var item in items) displayIndexList.Add(int.Parse(item)); + for (var cnt = 0; cnt < dgv.Columns.Count; cnt++) + { + var index = displayIndexList.IndexOf(cnt); + dgv.Columns[index].DisplayIndex = cnt; + } + + // 列の表示/非表示の設定 + csv = IniHelper.ReadValue(null, dgv.Parent.Name + "." + dgv.Name, KEY_NAME_COLUMN_VISIBLE_CSV); + if (string.IsNullOrEmpty(csv)) return; + items = csv.Split(','); + if (items.Length != dgv.Columns.Count) return; + List visibleList = new List(); + foreach (var item in items) visibleList.Add(bool.TryParse(item, out var boolValue) ? boolValue : true); + for (var cnt = 0; cnt < dgv.Columns.Count; cnt++) + { + dgv.Columns[cnt].Visible = visibleList[cnt]; + } + } + + public static void MoveRow(DataGridView dgv, int rowIndex, bool moveUp) + { + // 移動先のインデックスを計算 + int targetIndex = moveUp ? rowIndex - 1 : rowIndex + 1; + + // 範囲チェック(一番上より上、または一番下より下には行かない) + if (targetIndex < 0 || targetIndex >= dgv.Rows.Count - (dgv.AllowUserToAddRows ? 1 : 0)) + { + return; + } + + // 選択状態などを保持したい場合の準備 + DataGridViewRow row = dgv.Rows[rowIndex]; + + // 一旦削除して挿入し直す + dgv.Rows.RemoveAt(rowIndex); + dgv.Rows.Insert(targetIndex, row); + + // 移動後の行を選択状態にする(ユーザー体験向上) + dgv.ClearSelection(); + dgv.Rows[targetIndex].Selected = true; + dgv.CurrentCell = dgv.Rows[targetIndex].Cells[0]; + } + + private const string INI_KEY_DGV_ROW_HEADER_HEIGHT = "DgvRowHeaderHeight"; + + /// + /// ヘッダ行の高さ保存 + /// + /// + public static void DgvSaveHeaderHeight(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + IniHelper.WriteValue(null, section, INI_KEY_DGV_ROW_HEADER_HEIGHT, dgv.ColumnHeadersHeight.ToString()); + } + + /// + /// ヘッダ行の高さロード + /// + /// + public static void DgvLoadHeaderHeight(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var str = IniHelper.ReadValue(null, section, INI_KEY_DGV_ROW_HEADER_HEIGHT, dgv.ColumnHeadersHeight.ToString()); + dgv.ColumnHeadersHeight = int.TryParse(str, out var intValue) ? intValue : dgv.ColumnHeadersHeight; + } + + /// + /// 非表示列の再表示 + /// + /// + public static void DgvRedispAllColumn(DataGridView dgv) + { + foreach (DataGridViewColumn col in dgv.Columns) col.Visible = true; + } + + private const string INI_KEY_COL_VISIBLE_CSV = "ColVisibleCsv"; + + /// + /// 列の表示非表示を保存 + /// + /// + public static void DgvSaveColVisible(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = new List(); + foreach (DataGridViewColumn col in dgv.Columns) list.Add(col.Visible); + IniHelper.WriteValue(null, section, INI_KEY_COL_VISIBLE_CSV, string.Join(",", list)); + } + + /// + /// 列の表示非表示をロード + /// + /// + public static void DgvLoadColVisible(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = IniHelper.ReadValue(null, section, INI_KEY_COL_VISIBLE_CSV).Split(',').ToList(); + foreach (DataGridViewColumn col in dgv.Columns) + { + if (list.Count - 1 < col.Index) break; + if (!bool.TryParse(list[col.Index], out bool val)) break; + col.Visible = val; + } + } + + private const string INI_KEY_COL_DISPLAY_INDEX_CSV = "ColDisplayIndexCsv"; + + /// + /// 列の表示順インデックスを保存 + /// + /// + public static void DgvSaveColDisplayIndex(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = new List(); + foreach (DataGridViewColumn col in dgv.Columns) list.Add($"{col.Name}={col.DisplayIndex}"); + IniHelper.WriteValue(null, section, INI_KEY_COL_DISPLAY_INDEX_CSV, string.Join(",", list)); + } + + /// + /// 列の表示順インデックスをロード + /// + /// + public static void DgvLoadColDisplayIndex(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = IniHelper.ReadValue(null, section, INI_KEY_COL_DISPLAY_INDEX_CSV).Split(',').ToList(); + if (list.Count == 1 && string.IsNullOrEmpty(list[0])) return; + Dictionary dic = new Dictionary(); + foreach (var str in list) + { + dic.Add(int.Parse(str.Split('=')[1]), str.Split('=')[0]); + } + + // DisplayIndexは小さい方から設定しないと崩れる + foreach (var di in dic.Keys.OrderBy(m => m)) + { + dgv.Columns[dic[di]].DisplayIndex = di; + } + } + + /// + /// 列幅の自動調整 + /// + /// + public static void DgvColumnWidthAutoAdjust(DataGridView dgv) + { + dgv.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells); + } + + /// + /// 行高さの自動調整 + /// + /// + public static void DgvRowHeightAutoAdjust(DataGridView dgv) + { + dgv.AutoResizeRows(DataGridViewAutoSizeRowsMode.AllCells); + } + + /// + /// 列固定 + /// + /// + public static void DgvColumnFix(DataGridView dgv) + { + if (dgv.CurrentCell == null) return; + + // 1. 今クリックした列の「見た目の位置」を取得 + int targetDisplayIndex = dgv.CurrentCell.OwningColumn.DisplayIndex; + + // 2. いったん全列の固定を解除(これをしないとエラーになる場合がある) + DgvColumnFixKaijo(dgv); + + // 3. 見た目の位置(DisplayIndex)が、ターゲット以下の列をすべて固定! + // ※DisplayIndexの小さい順(左側から順)にFrozenをかけていくのがコツ + var targetColumns = dgv.Columns.Cast() + .OrderBy(c => c.DisplayIndex); + + foreach (var col in targetColumns) + { + if (col.DisplayIndex <= targetDisplayIndex) + { + col.Frozen = true; + } + } + } + + /// + /// 列固定解除 + /// + /// + public static void DgvColumnFixKaijo(DataGridView dgv) + { + // 2. いったん全列の固定を解除(これをしないとエラーになる場合がある) + foreach (DataGridViewColumn col in dgv.Columns) + { + col.Frozen = false; + } + } + + private const string INI_KEY_COL_FROZEN_CSV = "ColFrozenCsv"; + + /// + /// 列の固定を保存 + /// + /// + public static void DgvSaveColFrozen(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = new List(); + foreach (DataGridViewColumn col in dgv.Columns) list.Add(col.Frozen); + IniHelper.WriteValue(null, section, INI_KEY_COL_FROZEN_CSV, string.Join(",", list)); + } + + /// + /// 列の固定をロード + /// + /// + public static void DgvLoadColFrozen(DataGridView dgv) + { + var section = dgv.Parent.Name + "." + dgv.Name; + var list = IniHelper.ReadValue(null, section, INI_KEY_COL_FROZEN_CSV).Split(',').ToList(); + foreach (DataGridViewColumn col in dgv.Columns) + { + if (list.Count - 1 < col.Index) break; + if (!bool.TryParse(list[col.Index], out bool val)) break; + col.Frozen = val; + } + } + + private const string INI_KEY_DGV_HEADER_BACK_COLOR = "DgvHeaderBackColor"; + + /// + /// ヘッダ背景色選択 + /// + /// + /// + public static void DgvSelectHeaderBackColor(Form form, DataGridView dgv) + { + using (ColorDialog cd = new ColorDialog()) + { + // 今の色を初期選択にしておく + cd.Color = dgv.ColumnHeadersDefaultCellStyle.BackColor; + + if (cd.ShowDialog() == DialogResult.OK) + { + // 1. フォームの背景色を即時変更 + dgv.ColumnHeadersDefaultCellStyle.BackColor = cd.Color; + + // 文字色を計算して算出し設定 + dgv.ColumnHeadersDefaultCellStyle.ForeColor = ColorHelper.GetContrastColor(dgv.ColumnHeadersDefaultCellStyle.BackColor); + + // 2. 🔥 Colorを16進数(#RRGGBB)に変換してINIに書き込み + string colorHex = ColorTranslator.ToHtml(cd.Color); + IniHelper.WriteValue(null, form.Name, INI_KEY_DGV_HEADER_BACK_COLOR, colorHex); + } + } + } + + /// + /// ヘッダ背景色(名前でマッチした列のみ)選択 + /// + /// + /// + /// + /// + public static void DgvSelectHeaderBackColorWithNameMatch(Form form, DataGridView dgv, string colNamePattern, string iniKey) + { + // マッチする列を探し、現在の色を採取 + Color? color = null; + foreach (DataGridViewColumn col in dgv.Columns) + { + if (StringUxer.WildcardMatch(col.Name, colNamePattern)) + { + color = col.HeaderCell.Style.BackColor; + } + } + if (color == null) + { + MessageBox.Show(form, $"パターンにマッチする列なし[パターン:{colNamePattern}]"); + return; + } + + using (ColorDialog cd = new ColorDialog()) + { + // 今の色を初期選択にしておく + cd.Color = (Color)color; + + if (cd.ShowDialog() == DialogResult.OK) + { + var backColor = cd.Color; + // 文字色を計算して算出し設定 + var foreColor = ColorHelper.GetContrastColor(backColor); + + // DGVに反映 + foreach (DataGridViewColumn col in dgv.Columns) + { + if (StringUxer.WildcardMatch(col.Name, colNamePattern)) + { + col.HeaderCell.Style.BackColor = backColor; + col.HeaderCell.Style.ForeColor = foreColor; + } + } + + // 2. 🔥 Colorを16進数(#RRGGBB)に変換してINIに書き込み + string colorHex = ColorTranslator.ToHtml(cd.Color); + IniHelper.WriteValue(null, form.Name, iniKey, colorHex); + } + } + } + + /// + /// ヘッダ背景色をINIから取得して設定 + /// + /// + public static void DgvSetHeaderBackColorFromIni(Form form, DataGridView dgv) + { + var colorHex = IniHelper.ReadValue(null, form.Name, INI_KEY_DGV_HEADER_BACK_COLOR, ColorTranslator.ToHtml(dgv.ColumnHeadersDefaultCellStyle.BackColor)); + dgv.ColumnHeadersDefaultCellStyle.BackColor = ColorTranslator.FromHtml(colorHex); + // 文字色を計算して算出し設定 + dgv.ColumnHeadersDefaultCellStyle.ForeColor = ColorHelper.GetContrastColor(dgv.ColumnHeadersDefaultCellStyle.BackColor); + } + + /// + /// ヘッダ背景色をINIから取得して設定(名前マッチング) + /// + /// + /// + /// + /// + public static void DgvSetHeaderBackColorFromIniWithNameMatch(Form form, DataGridView dgv, string colNamePattern, string iniKey) + { + // マッチする列を探し、現在の色を採取 + Color? color = null; + foreach (DataGridViewColumn col in dgv.Columns) + { + if (StringUxer.WildcardMatch(col.Name, colNamePattern)) + { + color = col.HeaderCell.Style.BackColor; + } + } + if (color == null) + { + MessageBox.Show(form, $"パターンにマッチする列なし[パターン:{colNamePattern}]"); + return; + } + + // INIから取得 + var colorHex = IniHelper.ReadValue(null, form.Name, iniKey, ColorTranslator.ToHtml((Color)color)); + var backColor = ColorTranslator.FromHtml(colorHex); + var foreColor = ColorHelper.GetContrastColor(backColor); + + // マッチする列に設定 + foreach (DataGridViewColumn col in dgv.Columns) + { + if (StringUxer.WildcardMatch(col.Name, colNamePattern)) + { + col.HeaderCell.Style.BackColor = backColor; + col.HeaderCell.Style.ForeColor = foreColor; + } + } + } + + public const string TSV_PATH = @".\@@@DGV@@@.tsv"; + + /// + /// DGVデータTSV保存 + /// + /// + /// + public static void SaveGridDataToTsv(Form form, DataGridView dgv) + { + var path = TSV_PATH.Replace("@@@DGV@@@", $"{form.Name}.{dgv.Name}"); + try + { + using (StreamWriter sw = new StreamWriter(path, false, Encoding.UTF8)) + { + foreach (DataGridViewRow row in dgv.Rows) + { + // 新規行(入力用の空行)はスキップ + if (row.IsNewRow) continue; + + // 各セルの値をタブで結合 + var cells = row.Cells.Cast().Select(c => c.Value?.ToString() ?? ""); + sw.WriteLine(string.Join("\t", cells)); + } + } + } + catch (Exception ex) + { + MessageBox.Show("DGVのTSV保存に失敗しました: " + ex.Message); + } + } + public static void LoadGridDataToTsv(Form form, DataGridView dgv) + { + var path = TSV_PATH.Replace("@@@DGV@@@", $"{form.Name}.{dgv.Name}"); + if (!File.Exists(path)) return; + try + { + dgv.Rows.Clear(); + string[] lines = File.ReadAllLines(path, Encoding.UTF8); + + foreach (string line in lines) + { + if (string.IsNullOrWhiteSpace(line)) continue; + + // タブで分割して行を追加 + string[] values = line.Split('\t'); + dgv.Rows.Add(values); + } + } + catch (Exception ex) + { + MessageBox.Show("DGVのTSVによる復元に失敗しました: " + ex.Message); + } + } + + public static void SearchGrid( + DataGridView dgv, + string searchText, + bool forward) + { + if (string.IsNullOrEmpty(searchText)) return; + + int rowCount = dgv.RowCount; + int colCount = dgv.ColumnCount; + + // 現在のセルの位置を取得(選択されていなければ 0,0 から) + int startRow = dgv.CurrentCell?.RowIndex ?? 0; + int startCol = dgv.CurrentCell?.ColumnIndex ?? 0; + + // 全セルをフラットなインデックスとして考えて計算 + int totalCells = rowCount * colCount; + int startIndex = (startRow * colCount) + startCol; + + for (int i = 1; i < totalCells; i++) + { + // forwardがtrueならプラス、falseならマイナス方向にずらす + int offset = forward ? i : -i; + int nextIndex = (startIndex + offset + totalCells) % totalCells; + + int r = nextIndex / colCount; + int c = nextIndex % colCount; + + DataGridViewCell cell = dgv[c, r]; + + if (cell.Value != null && cell.Value.ToString().Contains(searchText)) + { + dgv.CurrentCell = cell; + return; // 見つかったら終了 + } + } + + MessageBox.Show("他には見つかりませんでしたにゃ!🐾"); + } + } +} diff --git a/Forms/FormHelper.cs b/Forms/FormHelper.cs new file mode 100644 index 0000000..ec24d17 --- /dev/null +++ b/Forms/FormHelper.cs @@ -0,0 +1,175 @@ +using KssSmaPlaLib.Ini; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms +{ + public static class FormHelper + { + /// + /// フォームのコンストラクタで行う処理集 + /// + /// + public static void FormConstructorProc(Form frm) + { + frm.KeyPreview = true; + FormHelper.SetTitle(frm); + foreach (var control in frm.Controls) + { + if (control is DataGridView) + { + // DGVで行う共通処理 + var dgv = control as DataGridView; + dgv.Dock = DockStyle.Fill; + dgv.AllowUserToResizeRows = false; + dgv.AllowUserToAddRows = false; + dgv.AllowUserToDeleteRows = false; + dgv.AllowUserToOrderColumns = false; + dgv.AllowUserToResizeColumns = true; + dgv.AllowUserToResizeRows = false; + dgv.ReadOnly = true; + dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + dgv.RowHeadersVisible = false; + dgv.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; + // 列ヘッダのユーザによるソートを無効化する + foreach (DataGridViewColumn col in dgv.Columns) col.SortMode = DataGridViewColumnSortMode.NotSortable; + } + } + } + + /// + /// フォームのロードで行う処理集 + /// + /// + /// + public static void FormLoadProc(Form frm, ContextMenuStrip[] menus, bool isDgvColAutoFit = false) + { + FormHelper.LoadFormBounds(frm); + foreach (var control in frm.Controls) + { + if (control is DataGridView) + { + // DGVで行う共通処理 + var dgv = control as DataGridView; + DgvHelper.DgvLoadColVisible(dgv); + DgvHelper.DgvLoadColDisplayIndex(dgv); + DgvHelper.DgvLoadColFrozen(dgv); + if (isDgvColAutoFit) + { + DgvHelper.DgvColumnWidthAutoAdjust(dgv); + } + else + { + DgvHelper.DgvLoadColWidth(dgv); + } + DgvHelper.DgvLoadFontFromIni(dgv); + } + } + foreach (var menu in menus) + { + ContextMenuHelper.SetFont(frm, menu); + } + } + + /// + /// フォーム閉じるときの処理集 + /// + /// + public static void FormClosingProc(Form frm) + { + FormHelper.SaveFormBounds(frm); + foreach (var control in frm.Controls) + { + if (control is DataGridView) + { + // DGVで行う共通処理 + var dgv = control as DataGridView; + DgvHelper.DgvSaveColVisible(dgv); + DgvHelper.DgvSaveColDisplayIndex(dgv); + DgvHelper.DgvSaveColFrozen(dgv); + DgvHelper.DgvSaveColWidth(dgv); + DgvHelper.DgvSaveFontToIni(dgv); + } + } + } + + private const string INI_KEY_RESTORE_BOUNDS_CSV = "RestoreBoundsCsv"; + + /// + /// フォームのバウンズ保存 + /// ※フォームのClosingイベントでCallしてください。 + /// + /// + public static void SaveFormBounds(Form form) + { + var csv = string.Empty; + if (form.WindowState == FormWindowState.Maximized || form.WindowState == FormWindowState.Minimized) + { + csv = $"{form.RestoreBounds.Width},{form.RestoreBounds.Height},{form.RestoreBounds.Top},{form.RestoreBounds.Left},{form.WindowState == FormWindowState.Maximized}"; + } + else + { + csv = $"{form.Bounds.Width},{form.Bounds.Height},{form.Bounds.Top},{form.Bounds.Left},{form.WindowState == FormWindowState.Maximized}"; + } + IniHelper.WriteValue(null, form.Name, INI_KEY_RESTORE_BOUNDS_CSV, csv); + } + + /// + /// フォームのバウンズ復元 + /// ※フォームのLoadイベントでCallしてください。 + /// + /// + public static void LoadFormBounds(Form form) + { + var csv = IniHelper.ReadValue(null, form.Name, INI_KEY_RESTORE_BOUNDS_CSV); + if (string.IsNullOrEmpty(csv)) return; + var items = csv.Split(','); + if (items.Length != 5) return; + if (!int.TryParse(items[0], out var width)) return; + if (!int.TryParse(items[1], out var height)) return; + if (!int.TryParse(items[2], out var top)) return; + if (!int.TryParse(items[3], out var left)) return; + if (!bool.TryParse(items[4], out var isMaximum)) return; + + form.Bounds = new System.Drawing.Rectangle(left, top, width, height); + if (isMaximum) form.WindowState = FormWindowState.Maximized; + } + + private const string KEY_NAME_TITLE = "Title"; + + /// + /// タイトル設定(INI優先) + /// ※フォームのコンストラクタでCallしてください + /// + /// + public static void SetTitle(Form form) + { + form.Text = IniHelper.ReadValue(null, form.Name, KEY_NAME_TITLE, form.Text); + } + + /// + /// フォームの画面キャプチャを取得し画像ファイルに保存する + /// + /// + /// + public static void GetCaptureAndSaveBitmap(Form form, string bitmapFilePath) + { + // フォームと同じサイズのBitmapを作成 + Bitmap bitmap = new Bitmap(form.Width, form.Height); + + // Bitmapにフォームの内容を描画 + form.DrawToBitmap(bitmap, new Rectangle(0, 0, form.Width, form.Height)); + + // 画像ファイルとして保存 + bitmap.Save(bitmapFilePath, System.Drawing.Imaging.ImageFormat.Jpeg); + + // リソースの解放 + bitmap.Dispose(); + } + } +} diff --git a/Forms/ToastNotifier.cs b/Forms/ToastNotifier.cs new file mode 100644 index 0000000..b2ea235 --- /dev/null +++ b/Forms/ToastNotifier.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Forms +{ + public static class ToastNotifier + { + private const int WS_EX_NOACTIVATE = 0x08000000; + private const int WS_EX_TOPMOST = 0x00000008; + + //public static void Show(Form owner, string message, Color backColor, int durationMs = 3000) + //{ + // // 1. フォームの基本設定 + // Form toast = new Form + // { + // FormBorderStyle = FormBorderStyle.None, + // StartPosition = FormStartPosition.Manual, + // BackColor = backColor, + // ShowInTaskbar = false, + // TopMost = true, + // Size = new Size(300, 60), + // Opacity = 0, + // Enabled = false // トースト自体は操作不能にしておく + // }; + // Label lbl = new Label + // { + // Text = message, + // ForeColor = Color.White, + // Font = new Font("MS UI Gothic", 10, FontStyle.Bold), + // Dock = DockStyle.Fill, + // TextAlign = ContentAlignment.MiddleCenter + // }; + // toast.Controls.Add(lbl); + // // オーナーの右下に配置 + // toast.Location = new Point( + // owner.Location.X + owner.Width - toast.Width - 20, + // owner.Location.Y + owner.Height - toast.Height - 20 + // ); + // // 2. タイマーでアニメーションを管理 + // Timer timer = new Timer { Interval = 10 }; + // DateTime startTime = DateTime.Now; + // timer.Tick += (s, e) => + // { + // double elapsed = (DateTime.Now - startTime).TotalMilliseconds; + // if (elapsed < 200) // 最初の0.2秒でフェードイン + // { + // toast.Opacity = elapsed / 200.0; + // } + // else if (elapsed < 200 + durationMs) // 指定時間はそのまま + // { + // toast.Opacity = 1.0; + // } + // else if (elapsed < 200 + durationMs + 200) // 最後の0.2秒でフェードアウト + // { + // toast.Opacity = 1.0 - (elapsed - (200 + durationMs)) / 200.0; + // } + // else // 終了処理 + // { + // timer.Stop(); + // timer.Dispose(); + // toast.Close(); + // toast.Dispose(); + // } + // }; + // // 3. 表示開始(ここを通った瞬間にメソッドは終了し、呼び出し元に戻ります) + // toast.Show(); + // timer.Start(); + //} + + //public static async Task ShowAsync(Form owner, string message, Color backColor, int durationMs = 3000) + //{ + // // 1. フォームの作成 + // using (var toast = new Form + // { + // FormBorderStyle = FormBorderStyle.None, + // StartPosition = FormStartPosition.Manual, + // BackColor = backColor, + // ShowInTaskbar = false, + // TopMost = true, + // Size = new Size(300, 60), + // Opacity = 0 + // }) + // { + // // フォーカスを奪わないための設定(WinAPIをラップしたCreateParamsの上書き) + // // ※簡易的にリフレクションで設定するか、Formを継承したクラスを作るのが定石です + // Label lbl = new Label + // { + // Text = message, + // ForeColor = Color.White, + // Font = new Font("MS UI Gothic", 10, FontStyle.Bold), + // Dock = DockStyle.Fill, + // TextAlign = ContentAlignment.MiddleCenter + // }; + // toast.Controls.Add(lbl); + // toast.Location = new Point( + // owner.Location.X + owner.Width - toast.Width - 20, + // owner.Location.Y + owner.Height - toast.Height - 20 + // ); + // // 表示(フォーカスを奪わずに表示) + // toast.Show(); + // // フォーカスが移るのを防ぐための念押し(Activateさせない) + // // 2. アニメーション(async/await の真骨頂) + // for (int i = 0; i < 10; i++) { toast.Opacity += 0.1; await Task.Delay(10); } + // await Task.Delay(durationMs); + // for (int i = 0; i < 10; i++) { toast.Opacity -= 0.1; await Task.Delay(10); } + // toast.Close(); + // } + //} + + public static async Task ShowAsync(Form owner, string message, Color backColor, int durationMs = 3000) + { + // 【重要】SignalRなどの別スレッドから呼ばれても安全なように、UIスレッドへ強制バトンタッチ + if (owner.InvokeRequired) + { + owner.BeginInvoke(new Action(() => { _ = ShowAsync(owner, message, backColor, durationMs); })); + return; + } + + // 1. フォームの作成(usingは使わず、Closeに任せる) + // フォーカスを奪わない(WS_EX_NOACTIVATE = 0x08000000)を仕込んだカスタムフォームをその場で定義 + var toast = new NoActivateForm + { + FormBorderStyle = FormBorderStyle.None, + StartPosition = FormStartPosition.Manual, + BackColor = backColor, + ShowInTaskbar = false, + TopMost = true, + Size = new Size(300, 60), + Opacity = 0 + }; + + Label lbl = new Label + { + Text = message, + ForeColor = Color.White, + Font = new Font("MS UI Gothic", 10, FontStyle.Bold), + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleCenter + }; + toast.Controls.Add(lbl); + + // 右下のベストポジションに配置(オーナーフォーム基準) + toast.Location = new Point( + owner.Location.X + owner.Width - toast.Width - 20, + owner.Location.Y + owner.Height - toast.Height - 20 + ); + + // 表示(フォーカスを絶対に奪わない) + toast.Show(); + + // 2. アニメーション(async/await の真骨頂!) + for (int i = 0; i < 10; i++) { toast.Opacity += 0.1; await Task.Delay(10); } + + await Task.Delay(durationMs); + + for (int i = 0; i < 10; i++) { toast.Opacity -= 0.1; await Task.Delay(10); } + + // 閉じると同時に、WinFormsが自動でメモリを綺麗に解放(Dispose)してくれます + toast.Close(); + } + } + + // 【職人技】フォーカスを絶対に奪わないフォーム(CreateParamsの上書き) + public class NoActivateForm : Form + { + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x08000000; // WS_EX_NOACTIVATE(クリックしてもフォーカスが移らない魔法のフラグ) + return cp; + } + } + } +} diff --git a/IO/Database/DbUxer.cs b/IO/Database/DbUxer.cs new file mode 100644 index 0000000..ad11157 --- /dev/null +++ b/IO/Database/DbUxer.cs @@ -0,0 +1,684 @@ +using Npgsql; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Office.Word; +using DocumentFormat.OpenXml.Spreadsheet; +using DocumentFormat.OpenXml.Wordprocessing; +using KssSmaPlaLib.Commons; +using Serilog; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Data.Entity; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using static KssSmaPlaLib.IO.Database.DbUxer; +using Path = System.IO.Path; + +namespace KssSmaPlaLib.IO.Database +{ + /// + /// DBアクセスアシスタント + /// + public class DbUxer : IDisposable + { + public enum DbProvider + { + PostgreSQL, + SQLServer, + MySQL, + Oracle, + SQLite, + } + + /// + /// プロバイダ名ディクショナリ + /// + private static readonly Dictionary _providerNameDic = new Dictionary() + { + { DbProvider.PostgreSQL, "Npgsql" }, + { DbProvider.SQLServer, "System.Data.SqlClient" }, + { DbProvider.MySQL, "MySql.Data.MySqlClient" }, + { DbProvider.Oracle, "Oracle.ManagedDataAccess.Client" }, + { DbProvider.SQLite, "System.Data.SQLite" }, + }; + + /// + /// プロバイダ名(文字列)からenum値に変更 + /// + /// プロバイダ名(文字列) + /// enum値(該当なし時はnull) + public static DbProvider? GetDbProviderEnumFromString(string dbProviderName = "PostgreSQL") + { + switch(dbProviderName.ToLower().Replace(" ","")) + { + case "postgresql": + case "ポスグレ": + return DbProvider.PostgreSQL; + case "sqlserver": + return DbProvider.SQLServer; + case "mysql": + return DbProvider.MySQL; + case "oracle": + case "oracledatabase": + return DbProvider.Oracle; + case "sqlite": + case "sqlite3": + return DbProvider.SQLite; + default: + return null; + } + } + + /// + /// コミット済みフラグ + /// + private bool _isCommitted = false; + + /// + /// SQLディクショナリ + /// + private static readonly Dictionary _sqlDic = new Dictionary(); + + /// + /// 指定フォルダからSQLファイルを読み込む(ディクショナリ保存) + /// + /// フォルダパス + /// 成否 + public static bool TryLoadSqlFromText(string folderPath, out string resultMessage) + { + resultMessage = string.Empty; + var fileCount = 0; + List files = new List(); + + try + { + if (string.IsNullOrEmpty(folderPath)) + { + resultMessage = "フォルダパスが指定されていません。"; + return false; + } + if (!Directory.Exists(folderPath)) + { + resultMessage = $"フォルダが存在しません。[フォルダパス:{folderPath}]"; + return false; + } + _sqlDic.Clear(); + foreach (var file in Directory.GetFiles(folderPath, "*.sql.txt")) + { + _sqlDic.Add(Path.GetFileNameWithoutExtension(file).ToLower().Replace(".sql", ""), System.IO.File.ReadAllText(file)); + files.Add(Path.GetFileNameWithoutExtension(file).Replace(".sql", "")); + fileCount++; + } + resultMessage = $"SQLファイルをロードしました。[ファイル数:{fileCount},ファイル:{string.Join(",", files)}]"; + return true; + } + catch (Exception ex) + { + resultMessage = $"SQLファイル読み込み時にエラーが発生しました。[エラー内容:{ex.Message}]"; + return false; + } + } + + public static bool TestConnectByDic(Dictionary iniDic, out string resultMessage) + { + const string KEY_DB_PROVIDER_NAME = "DbProviderName"; + const string KEY_DB_CONNECTION_STRING = "DbConnectionString"; + + var dbProviderName = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_PROVIDER_NAME, string.Empty); + var connectionString = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_CONNECTION_STRING, string.Empty); + return TestConnect(dbProviderName, connectionString, out resultMessage); + } + + /// + /// テスト接続 + /// + /// 成否 + public static bool TestConnect(string dbPrividerName, string dbConnectionString, out string resultMessage) + { + // パラメータチェック&DBプロバイダ取得 + if (!TryConnectParameter(dbPrividerName, dbConnectionString, out DbProvider? _, out resultMessage)) + { + return false; + } + + if (!TryCreate(dbPrividerName, dbConnectionString, out var dbUxer, out resultMessage, false)) + { + return false; + } + dbUxer.Dispose(); + return true; + } + + /// + /// DB接続 + /// + private DbConnection _con = null; + + /// + /// トランザクション + /// + private DbTransaction _tran = null; + + public static bool TryCreateByDic(Dictionary iniDic, out DbUxer dbUxer, out string resultMessage, bool useTransaction = false) + { + const string KEY_DB_PROVIDER_NAME = "DbProviderName"; + const string KEY_DB_CONNECTION_STRING = "DbConnectionString"; + + var dbProviderName = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_PROVIDER_NAME, string.Empty); + var connectionString = DictionaryUxer.GetValueOrDefault(iniDic, KEY_DB_CONNECTION_STRING, string.Empty); + + return TryCreate(dbProviderName, connectionString, out dbUxer, out resultMessage, useTransaction); + } + + /// + /// 生成挑戦 + /// + /// + /// + /// + /// + /// + /// + public static bool TryCreate(string dbPrividerName, string dbConnectionString, out DbUxer dbUxer, out string resultMessage, bool useTransaction = false) + { + dbUxer = null; + + // パラメータチェック&DBプロバイダ取得 + if (!TryConnectParameter(dbPrividerName, dbConnectionString, out DbProvider? dbp, out resultMessage)) + { + return false; + } + + DbProvider dbpp = (DbProvider)dbp; + + try + { + // 生成 + dbUxer = new DbUxer(dbpp, dbConnectionString, useTransaction); + return true; + } + catch (Exception ex) + { + // 接続NG回答 + resultMessage = $"DB接続に失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + + private static bool TryConnectParameter(string dbPrividerName, string dbConnectionString, out DbProvider? dbp, out string resultMessage) + { + dbp = DbProvider.PostgreSQL; + resultMessage = string.Empty; + + if (string.IsNullOrEmpty(dbPrividerName)) + { + // DBプロバイダ指定なしの場合、デフォルトはポスグレとする + // Infoメッセージ + resultMessage = "DBプロバイダ名が未指定のため、デフォルトのPostgreSQLを使用します。"; + } + + if (string.IsNullOrEmpty(dbConnectionString)) + { + resultMessage = "接続文字列が設定されていません。"; + return false; + } + + if (!string.IsNullOrEmpty(dbPrividerName)) + { + dbp = GetDbProviderEnumFromString(dbPrividerName); + } + + if (dbp == null) + { + resultMessage = $"DBプロバイダ名が不正です。[DBプロバイダ名:{dbPrividerName}]"; + return false; + } + + return true; + } + + /// + /// コンストラクタ(DB接続) + /// + /// DBプロバイダ(enum値) + /// 接続文字列 + /// トランザクション有無 + private DbUxer(DbProvider dbProvider, string connectionString, bool useTransaction = false) + { + try + { + // DBプロバイダファクトリー作成 + DbProviderFactory factory; + if (dbProvider == DbProvider.PostgreSQL) + { + // ポスグレの場合、Npgsqlのインスタンをを直接取得 + factory = Npgsql.NpgsqlFactory.Instance; + } + else + { + factory = DbProviderFactories.GetFactory(_providerNameDic[dbProvider]); + } + + // DB接続作成 + _con = factory.CreateConnection(); + + // 接続文字列設定 + _con.ConnectionString = connectionString; + + // 接続 + _con.Open(); + + // トランザクション + if (useTransaction) _tran = _con.BeginTransaction(); + } + catch (Exception ex) + { + LoggerUxer.Error($"DB接続に失敗しました。[プロバイダ:{dbProvider},接続文字列:{connectionString},エラー内容:{ex.Message}]"); + throw ex; + } + } + + public bool ExecuteNonQuery(string sqlOrName, Dictionary parameters, out int resultCount, out string resultMessage) + { + resultCount = 0; + resultMessage = string.Empty; + + if (_con == null || _con.State != ConnectionState.Open) + { + resultMessage = "DBと接続されていません。"; + return false; + } + + try + { + using (var cmd = _con.CreateCommand()) + { + // コパンドプロパティを注入 + if (!TryInjectSoulToCmd(cmd, sqlOrName, parameters, out var messageForMe)) + { + resultMessage = messageForMe; + return false; + } + + resultCount = cmd.ExecuteNonQuery(); + LoggerUxer.Info($"[Rec Count:{resultCount}]"); + return true; + } + } + catch (Exception ex) + { + resultMessage = $"SQL実行でエラーが発生しました。[SQL名:{sqlOrName},エラー内容:{ex.Message}]"; + return false; + } + } + + public bool BeginTextImport( + string sqlOrName, + string csvFilePath, + out int resultCount, + out string resultMessage) + { + resultCount = 0; + resultMessage = string.Empty; + + if (_con == null || _con.State != ConnectionState.Open) + { + resultMessage = "DBと接続されていません。"; + return false; + } + + try + { + // SQLの取得 + sqlOrName = sqlOrName.ToLower(); + var sql = _sqlDic.ContainsKey(sqlOrName) ? _sqlDic[sqlOrName] : sqlOrName; + + // CSV有無 + if (!System.IO.File.Exists(csvFilePath)) + { + resultMessage = $"CSVファイルが存在しません。[ファイルパス:{csvFilePath}]"; + return false; + } + + // バルクインサート用のメソッドはポスグレの接続しか持たない + var pgConn = (NpgsqlConnection)_con; + + var sqls = sql.Split(new[] { "-- SPLIT --" }, StringSplitOptions.RemoveEmptyEntries); + if (sqls.Length != 3) + { + resultMessage = $"バルク挿入SQLで分割数が正しくありません。[SQLファイル名:{sqlOrName},分割数:{sqls.Length}]"; + return false; + } + + // 1個目のSQL(一時テーブル作成) + if (!ExecuteNonQuery(sqls[0], new Dictionary(), out var _, out var messageForMe)) + { + resultMessage = $"バルク挿入SQL(1)が失敗しました。[詳細:{messageForMe}]"; + return false; + } + + // 2個目のSQL(一時テーブルにバルク挿入) + using (var writer = pgConn.BeginTextImport(sqls[1])) + { + using (var reader = new StreamReader(csvFilePath)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + writer.WriteLine(line); + //resultCount++; // ここで「1件、2件…」と数える!♨️ + } + } + } // これで完了!1000件なら「シュンッ!」で終わります + + // 3個目のSQL(一時テーブルから対象テーブルへコピー) + if (!ExecuteNonQuery(sqls[2], new Dictionary(), out resultCount, out messageForMe)) + { + resultMessage = $"バルク挿入SQL(2)が失敗しました。[詳細:{messageForMe}]"; + return false; + } + + LoggerUxer.Info($"[Rec Count:{resultCount}]"); + return true; + + } + catch (Exception ex) + { + resultMessage = $"SQL実行でエラーが発生しました。[SQL名:{sqlOrName},エラー内容:{ex.Message}]"; + return false; + } + } + + public bool ExecuteQuery(string sqlOrName, Dictionary parameters, out List> recordSet, out string resultMessage) + { + recordSet = new List>(); + resultMessage = string.Empty; + + if (_con == null || _con.State != ConnectionState.Open) + { + resultMessage = "DBと接続されていません。"; + return false; + } + + try + { + using (var cmd = _con.CreateCommand()) + { + // コパンドプロパティを注入 + if (!TryInjectSoulToCmd(cmd, sqlOrName, parameters, out var messageForMe)) + { + resultMessage = messageForMe; + return false; + } + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var row = new Dictionary(); + for (int i = 0; i < reader.FieldCount; i++) + { + row[reader.GetName(i)] = reader.GetValue(i); + } + recordSet.Add(row); + } + LoggerUxer.Info($"[Rec Count:{recordSet.Count}]"); + } + } + return true; + } + catch (Exception ex) + { + resultMessage = $"DBからデータ取得時にエラーが発生しました。[SQL名:{sqlOrName},エラー内容:{ex.Message}]"; + return false; + } + } + + private bool TryInjectSoulToCmd(DbCommand cmd, string sqlOrName, Dictionary parameters, out string resultMessage) + { + resultMessage = string.Empty; + + // SQLの取得 + sqlOrName = sqlOrName.ToLower(); + var sql = _sqlDic.ContainsKey(sqlOrName) ? _sqlDic[sqlOrName] : sqlOrName; + //LoggerUxer.Info($"[SQL:{sql}]"); + + // SQL中のコマンドパラメータ名取得 + var paraList = ExtractSqlParams(sql); + //oggerUxer.Info($"[SQL Para names:{string.Join(",", paraList)}]"); + + // コマンドパラメータ値の型変換 + if (!TryChangeParamTypeFromSqlParams(ref parameters, ref paraList, out var messageForMe)) + { + resultMessage = $"コマンドパラメータ値の型変換に失敗しました。[エラー内容:{messageForMe}]"; + return false; + } + + // SQL文中のコマンドパラメータ型定義をカット + cmd.CommandText = sql = CutParamTypeOfSQL(sql); + //LoggerUxer.Info($"[SQL(Param Type Cut):{sql}]"); + + // 再度、SQL中のコマンドパラメータ名取得 + paraList = ExtractSqlParams(sql); + //LoggerUxer.Info($"[SQL Para names(Type cut):{string.Join(",", paraList)}]"); + + // SQLに合わせたコマンドパラメータ登録 + foreach (var p in parameters) + { + if (!paraList.Contains(p.Key)) continue; + var param = cmd.CreateParameter(); + param.ParameterName = p.Key; + param.Value = p.Value; + cmd.Parameters.Add(param); + } + + //LoggerUxer.Info($"[SQL Params:{GetCommandParametersCsv(cmd)}]"); + + if (cmd.Parameters.Count != paraList.Count) + { + resultMessage = $"必要なパラメータが不足しています。[SQL名:{sqlOrName},SQL項目:{string.Join(",", paraList)}, CSV項目:{string.Join(",", parameters.Keys)}]"; + return false; + } + return true; + } + + public void Commit() + { + _tran?.Commit(); + _isCommitted = true; + } + + /// + /// 終了処理 + /// + /// + public void Dispose() + { + if (_tran != null && !_isCommitted) + { + Log.Warning("トランザクション未コミット → 自動ロールバックされました"); + _tran.Rollback(); // 明示的にロールバック + } + _con?.Dispose(); + _con = null; + _tran = null; + } + + public static List ExtractSqlParams(string sql) + { + //var matches = System.Text.RegularExpressions.Regex.Matches(sql, @":([a-zA-Z_][a-zA-Z0-9_]*)"); + //return matches.Cast() + // .Select(m => m.Groups[1].Value) + // .Distinct() + // .ToList(); + + //var result = new List(); + //var lines = sql.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + + //foreach (var line in lines) + //{ + // // コメント部分を除去 + // var codePart = line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line; + + // // パラメータ抽出 + // var matches = System.Text.RegularExpressions.Regex.Matches( + // codePart, + // @":(\[[a-zA-Z]\][a-zA-Z_][a-zA-Z0-9_]*)|:([a-zA-Z_][a-zA-Z0-9_]*)" + // ); + // foreach (System.Text.RegularExpressions.Match match in matches) + // { + // var paramName = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + // if (!result.Contains(paramName)) + // result.Add(paramName); + // } + //} + //return result; + + var result = new List(); + var lines = sql.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + + foreach (var line in lines) + { + // コメント部分を除去 + var codePart = line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line; + + // パラメータ抽出(::で始まるものは除外) + var matches = System.Text.RegularExpressions.Regex.Matches( + codePart, + @"(?(); + + foreach (DbParameter param in command.Parameters) + { + string name = param.ParameterName?.TrimStart('@'); // @を除去(任意) + object value = param.Value; + + string formattedValue; + + if (value == null || value == DBNull.Value) + { + formattedValue = "NULL"; + } + else if (value is string || value is char) + { + formattedValue = $"'{value}'"; // シングルクォートで括る + } + else if (value is DateTime dt) + { + formattedValue = $"'{dt:yyyy-MM-dd HH:mm:ss}'"; // 日付も文字列扱い + } + else + { + formattedValue = value.ToString(); + } + + list.Add($"{name}={formattedValue}"); + } + + return string.Join(",", list); + } + private static bool TryChangeParamTypeFromSqlParams(ref Dictionary parameters, ref List paraNameList, out string resultMessage) + { + resultMessage = string.Empty; + + foreach (var key in parameters.Keys.ToList()) + { + var name = paraNameList + .FirstOrDefault(m => m == key || m.Substring(0, 1) == "[" && m.Substring(3) == key); + if (name != null && name.Substring(0, 1) == "[") + { + switch (name.Substring(0, 3).ToUpper()) + { + case "[I]": + if (string.IsNullOrEmpty(parameters[key].ToString())) + { + parameters[key] = DBNull.Value; + } + else if (int.TryParse(parameters[key].ToString(), out int intValue)) + { + parameters[key] = intValue; + } + else + { + resultMessage = $"パラメータ値[I]型変換に失敗しました。[キー:{key}値:{parameters[key]}]"; + return false; + } + break; + //case "[IN": + // if (string.IsNullOrEmpty(parameters[key].ToString())) + // { + // parameters[key] = DBNull.Value; + // } + // else if (int.TryParse(parameters[key].ToString(), out intValue)) + // { + // parameters[key] = intValue; + // } + // else + // { + // resultMessage = $"パラメータ値[IN]型変換に失敗しました。[キー:{key}値:{parameters[key]}]"; + // return false; + // } + // break; + case "[F]": + if (double.TryParse(parameters[key].ToString(), out double doubleValue)) + { + parameters[key] = doubleValue; + } + else + { + resultMessage = $"パラメータ値[F]型変換に失敗しました。[キー:{key}値:{parameters[key]}]"; + return false; + } + break; + case "[D]": + if (DateTime.TryParse(parameters[key].ToString(), out DateTime dtValue)) + { + parameters[key] = dtValue; + } + else + { + resultMessage = $"パラメータ値[D]型変換に失敗しました。[キー:{key}値:{parameters[key]}]"; + return false; + } + break; + case "[S]": + parameters[key] = parameters[key].ToString(); + break; + } + } + } + return true; + } + + // SQL文中のコマンドパラメータ型定義をカット + private static string CutParamTypeOfSQL(string sql) + { + string[] cutItems = new string[] { "[I]", "[F]", "[D]", "[S]" }; + foreach (var cutItem in cutItems) + { + sql = sql.Replace(cutItem.ToUpper(), ""); + sql = sql.Replace(cutItem.ToLower(), ""); + } + return sql; + } + } +} diff --git a/IO/Excel/ExcelCellValidatorUxer.cs b/IO/Excel/ExcelCellValidatorUxer.cs new file mode 100644 index 0000000..346aed6 --- /dev/null +++ b/IO/Excel/ExcelCellValidatorUxer.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.IO.Excel +{ + public static class ExcelCellValidatorUxer + { + /// + /// ExcelのA1形式(例: "A1", "Z99", "AA1000")かどうかを判定します。 + /// + /// セル参照文字列 + /// 有効なA1形式ならtrue、無効ならfalse + public static bool IsValidA1Format(string cellRef) + { + if (string.IsNullOrWhiteSpace(cellRef)) + return false; + + // 正規表現でA1形式をチェック(列: A〜XFD、行: 1〜1048576) + var match = System.Text.RegularExpressions.Regex.Match( + cellRef.ToUpperInvariant(), + @"^([A-Z]{1,3})([1-9][0-9]{0,6})$" + ); + + if (!match.Success) + return false; + + string colPart = match.Groups[1].Value; + string rowPart = match.Groups[2].Value; + + // 列がXFD(Excel最大列)以内かチェック + if (ColumnNameToNumber(colPart) > 16384) + return false; + + // 行がExcel最大行以内かチェック + if (int.TryParse(rowPart, out int row)) + { + return row >= 1 && row <= 1048576; + } + + return false; + } + + /// + /// 列名(例: "A", "Z", "AA")を列番号に変換(例: 1, 26, 27) + /// + public static int ColumnNameToNumber(string columnName) + { + int sum = 0; + foreach (char c in columnName) + { + sum *= 26; + sum += (c - 'A' + 1); + } + return sum; + } + } +} diff --git a/IO/Excel/ExcelColumnHelperUxer.cs b/IO/Excel/ExcelColumnHelperUxer.cs new file mode 100644 index 0000000..44b261a --- /dev/null +++ b/IO/Excel/ExcelColumnHelperUxer.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.IO.Excel +{ + public static class ExcelColumnHelperUxer + { + public static string GetExcelColumnLetter(int columnNumber) + { + if (columnNumber < 1) + throw new ArgumentOutOfRangeException(nameof(columnNumber), "Column number must be >= 1"); + + string columnLetter = string.Empty; + while (columnNumber > 0) + { + columnNumber--; // Adjust for 0-indexed calculation + columnLetter = (char)('A' + (columnNumber % 26)) + columnLetter; + columnNumber /= 26; + } + return columnLetter; + } + } +} diff --git a/IO/Excel/XlUxer.cs b/IO/Excel/XlUxer.cs new file mode 100644 index 0000000..569bbc9 --- /dev/null +++ b/IO/Excel/XlUxer.cs @@ -0,0 +1,95 @@ +using ClosedXML.Excel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.IO.Excel +{ + public class XlUxer + { + public static IXLCell GCell(IXLWorksheet sheet, string a1Address) + { + var cell = sheet.Cell(a1Address); + return GCell(cell); + } + + public static IXLCell GCell(IXLWorksheet sheet, int row, int col) + { + var cell = sheet.Cell(row, col); + return GCell(cell); + } + + public static IXLCell GCell(IXLCell cell) + { + return cell.IsMerged() ? cell.MergedRange().FirstCell() : cell; + } + + public static bool TryValidateA1CellAddress(string a1Address, out string normalizedAddress) + { + normalizedAddress = null; + + if (string.IsNullOrWhiteSpace(a1Address)) + return false; + + // 正規表現でA1形式(例:A1, B2, AA10など)をチェック + var regex = new Regex(@"^[A-Z]{1,3}[1-9][0-9]*$", RegexOptions.IgnoreCase); + if (!regex.IsMatch(a1Address)) + return false; + + // 正規化(大文字化) + normalizedAddress = a1Address.ToUpperInvariant(); + return true; + } + + /// + /// Shorter + /// + /// + /// + public static int GetColNo(string colLetter) + { + if (!TryConvertColLetterToNumber(colLetter, out var no)) + { + return int.MinValue; + } + return no; + } + + /// + /// 列アルファベットを列番号(1オリジン)に変換 + /// + /// 列アルファベット + /// [出力]列番号(1オリジン) + /// 成否 + public static bool TryConvertColLetterToNumber(string colLetter, out int colNo) + { + colNo = int.MinValue; + + // バリデ(A~XFD) + if(!Regex.IsMatch(colLetter, @"^(?i)([A-Z]|[A-Z]{2}|[A-E][A-Z]{2})$")) + { + return false; + } + + // 大文字化 + colLetter = colLetter.ToUpper(); + + // アルファベット1文字ずつ見て列番号を算出 + int no = 0; + foreach (char c in colLetter) + { + no = no * 26 + (c - 'A' + 1); + } + + // 列番号範囲外なら失敗 + if (no > 16384 || no < 1) return false; + + // 成功 + colNo = no; + return true; + } + } +} diff --git a/IO/File/CSVUxer.cs b/IO/File/CSVUxer.cs new file mode 100644 index 0000000..ed7307f --- /dev/null +++ b/IO/File/CSVUxer.cs @@ -0,0 +1,158 @@ +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Exchange.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace KssSmaPlaLib.IO.File +{ + public class CsvUxer + { + public static bool ExportToCsv(List list, string filePath, out string resultMessage, EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom) where T : ICsvExportableUxer + { + resultMessage = string.Empty; + try + { + var lines = new List { list.FirstOrDefault()?.CsvHeader ?? "" }; + lines.AddRange(list.Select(item => item.ToCsvLine())); + System.IO.File.WriteAllLines(filePath, lines, EncodingUxer.GetEncoding(encodingType)); + return true; + } + catch (Exception ex) + { + resultMessage = $"CSV出力に失敗しました。[ファイルパス:{filePath},エラー内容:{ex.Message}]"; + return false; + } + } + + /// + /// CSVインポート + /// + /// CSVファイルパス + /// [出力]レコードセット + /// [出力]メッセージ + /// [オプション]エンコード + /// 成否 + public static bool ImportFromCsv(string csvFilePath, out List> recordSet, out string resultMessage, EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom) + { + recordSet = new List>(); + resultMessage = string.Empty; + + try + { + // 全行読み込み + var lines = System.IO.File.ReadAllLines(csvFilePath, EncodingUxer.GetEncoding(encodingType)); + + // データ行0件 + if (lines.Length < 2) return true; + + // ヘッダ行項目分割 + var header = Regex.Split(lines[0], ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); + for (var cnt = 0; cnt < header.Length; cnt++) header[cnt] = header[cnt].Trim('\"'); + + // データ行件数分 + for (int i = 1; i < lines.Length; i++) + { + // 項目分割 + var values = Regex.Split(lines[i], ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); + for (var cnt = 0; cnt < values.Length; cnt++) values[cnt] = values[cnt].Trim('\"'); + + if (values.Length != header.Length) + { + resultMessage = $"CSV読み込みでヘッダの項目数とデータの項目数が異なります。[CSVファイルパス:{csvFilePath},行番号:{i + 1},ヘッダ行項目数:{header.Length},データ行項目数:{values.Length}]"; + return false; + } + + var record = new Dictionary(); + + // 全項目 + for (int j = 0; j < header.Length && j < values.Length; j++) + { + var key = header[j].Trim('\"'); + var value = values[j].Trim('\"'); + record[key] = value; + } + recordSet.Add(record); + } + return true; + } + catch (Exception ex) + { + resultMessage = $"CSV読み込み処理でエラーが発生しました。[CSVファイルパス:{csvFilePath},エラー内容:{ex.Message}]"; + return false; + } + } + + public static string Escape(string value) => $"\"{value.Replace("\"", "\"\"")}\""; + public static string Join(params object[] values) => string.Join(",", values.Select(m => Escape(m == null ? "" : m.ToString()))); + + /// + /// CSVファイルを読み込み、ディクショナリに変換します。 + /// + /// CSVファイルのパス + /// 出力されるディクショナリ(ファイル無しの場合は空) + /// + /// 処理が成功した場合はtrue、例外発生時はfalse + public static bool TryGetDictionaryFromCsv( + string path, + out Dictionary dic, + out string resultMessage) + { + resultMessage = string.Empty; + + // 1. 最初に出力変数を空のディクショナリで初期化 + dic = new Dictionary(); + + try + { + // 2. ファイルが存在しない場合は空のままtrueで返す + if (!System.IO.File.Exists(path)) + { + return true; + } + + // 3. BOM付きUTF-8を指定してファイルを開く + // (Encoding.UTF8 はデフォルトでBOMの有無を自動判別して正しく読み込めます) + using (var reader = new StreamReader(path, Encoding.UTF8)) + { + while (!reader.EndOfStream) + { + string line = reader.ReadLine(); + + // 空行はスキップ + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + // カンマで分割 (単純な2列前提) + string[] parts = line.Split(','); + + // 最低1列(キー)があれば処理する(2列未満の対策) + if (parts.Length > 0) + { + string key = parts[0]; + + // 2列目があればその値、なければ空文字 + string value = parts.Length > 1 ? parts[1] : string.Empty; + + // 重複キーは後勝ち(インデクサを使うことで上書き保存) + dic[key] = value; + } + } + } + + return true; + } + catch (Exception ex) + { + // TODO: あたしがのちほどエラーメッセージ処理を入れる! + resultMessage = $"CSVファイル読み込みに失敗しました。[詳細:{ex.Message}]"; + return false; + } + } + } +} diff --git a/IO/File/FileUxer.cs b/IO/File/FileUxer.cs new file mode 100644 index 0000000..6d22ef1 --- /dev/null +++ b/IO/File/FileUxer.cs @@ -0,0 +1,141 @@ +using DocumentFormat.OpenXml.Vml; +using KssSmaPlaLib.Commons; +using KssSmaPlaLib.Exchange.Interfaces; +using Org.BouncyCastle.Asn1; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Path = System.IO.Path; + +namespace KssSmaPlaLib.IO.File +{ + public static class FileUxer + { + /// + /// テキストファイル出力(全テキスト一括) + /// + /// 全テキスト + /// ファイルパス + /// メッセージ + /// 文字コード + /// 成否 + public static bool WriteAllText(string allLineText, string filePath, out string resultMessage, EncodingUxer.EncodingType encodingType = EncodingUxer.EncodingType.Utf8WithoutBom) + { + resultMessage = string.Empty; + try + { + System.IO.File.WriteAllText(filePath, allLineText, EncodingUxer.GetEncoding(encodingType)); + return true; + } + catch (Exception ex) + { + resultMessage = $"CSV出力に失敗しました。[ファイルパス:{filePath},エラー内容:{ex.Message}]"; + return false; + } + } + + public static bool TryDeletePatternFiles( + string folderPath, + string fileNamePattern, + out int fileCount, + out string resultMessage) + { + fileCount = 0; + resultMessage = string.Empty; + if (!Directory.Exists(folderPath)) + { + resultMessage = $"フォルダが存在しません。[フォルダパス:{folderPath}]"; + return false; + } + try + { + var files = Directory.GetFiles(folderPath, fileNamePattern); + + foreach (var filePath in files) + { + System.IO.File.Delete(filePath); + fileCount++; + } + return true; + } + catch (Exception ex) + { + resultMessage = $"ファイル削除中にエラーが発生しました。[フォルダパス:{folderPath},ファイル名パターン:{fileNamePattern},エラー内容:{ex.Message}]"; + return false; + } + } + + public static string GetTimestampedFileName(string originalName, DateTime dt, string prefixDtFormat, string suffixDtFormat) + { + string nameWithoutExt = Path.GetFileNameWithoutExtension(originalName); + string ext = Path.GetExtension(originalName); + string prefixDtStr = dt.ToString(prefixDtFormat); + string suffixDtStr = dt.ToString(suffixDtFormat); + + string newName = prefixDtStr + nameWithoutExt + suffixDtStr; + return newName + ext; + } + + public static bool CheckParentFolderExists(string folderPath, out string parentPath) + { + parentPath = Path.GetDirectoryName(folderPath); + return Directory.Exists(parentPath); + } + + public static bool TryApplyActionToFiles( + string folderPath, + Func condition, + Action action, + out int fileCount, + out string resultMessage, + string actionName = "操作", + bool isDescending = true, + Func sortKey = null, + int skip = 0) + { + fileCount = 0; + resultMessage = string.Empty; + + try + { + // 条件未指定時は全件対象 + condition = condition ?? (_ => true); + + // 一旦条件で取得 + var files = new DirectoryInfo(folderPath) + .GetFiles() + .Where(condition); + + // ソートキー作成 + var keySelector = + sortKey != null ? (Func) + (f => sortKey(f)) : f => f.LastWriteTime; + + // ソート+スキップ+リスト化 + var filesList = + (isDescending ? + files.OrderByDescending(keySelector) : + files.OrderBy(keySelector)) + .Skip(skip) + .ToList(); + + foreach (var file in filesList) + { + action(file); + LoggerUxer.Info($"{actionName}しました。[{file}]"); + } + + fileCount = filesList.Count; + return true; + } + catch (Exception ex) + { + resultMessage = $"ファイル群の{actionName}に失敗しました。[エラー内容:{ex.Message}]"; + return false; + } + } + } +} diff --git a/IO/File/JsonFileUxer.cs b/IO/File/JsonFileUxer.cs new file mode 100644 index 0000000..d666448 --- /dev/null +++ b/IO/File/JsonFileUxer.cs @@ -0,0 +1,206 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.IO.File +{ + public static class JsonFileUxer + { + /// + /// JSONファイルのピンポイント1項目更新 + /// + /// JSONファイルパス + /// JSONノードパス + /// 更新値 + /// + public static void UpdateJsonKeyAtomic(string path, string jsonPath, object newValue) + { + //// 使い方例:パスワード更新 + //JsonFiileUxer.UpdateJsonKeyAtomic("config.json", "app.auth.password", "new_pw_2026-01"); + + var jtoken = JToken.FromObject(newValue); + + // 1) 読み込み(UTF-8) + var json = System.IO.File.ReadAllText(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + var jo = JObject.Parse(json); + + // 2) ピンポイント更新(途中が無ければ作る) + var token = jo.SelectToken(jsonPath); + if (token is null) + { + // 例: "app.auth.password" を分解して動的に作る + var parts = jsonPath.Split('.'); + JToken cur = jo; + for (int i = 0; i < parts.Length; i++) + { + var name = parts[i]; + var obj = cur as JObject ?? throw new InvalidOperationException("親がオブジェクトではありません"); + if (i == parts.Length - 1) + { + obj[name] = jtoken; + } + else + { + if (obj[name] == null) obj[name] = new JObject(); + cur = obj[name]; + } + } + } + else + { + token.Replace(jtoken); + } + + // 3) 一時ファイルへ書き出し + var dir = System.IO.Path.GetDirectoryName(path) ?? "."; + var tmp = System.IO.Path.Combine(dir, ".tmp_config.json"); + var backup = path + $"_{DateTime.Now.ToString("yyyyMMddHHmmss")}" + ".bak"; + + //var output = jo.ToString(Formatting.Indented) + Environment.NewLine; + var output = JsonConvert.SerializeObject(jo, Formatting.Indented) + Environment.NewLine; + System.IO.File.WriteAllText(tmp, output, new UTF8Encoding(false)); + + // 4) バックアップ作成 → 原子置換 + if (System.IO.File.Exists(path)) System.IO.File.Copy(path, backup, overwrite: true); + System.IO.File.Replace(tmp, path, backup); + } + + /// + /// JSONファイルから、jsonPathに一致する単一項目を取得します。 + /// 取得に成功したら true を返し、 out value に JToken を設定します。 + /// キーが見つからない・JSON無効・ファイル無し等は false を返し、 value は null。 + /// + public static bool TryGetJsonKey(string path, string jsonPath, out JToken value) + { + value = null; + + // 1) 読み込み(UTF-8 BOMなし) + string json; + try + { + json = System.IO.File.ReadAllText(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + } + catch (Exception) + { + // ファイルがない/読み取り不可 + return false; + } + + // 2) 解析 + JObject jo; + try + { + jo = JObject.Parse(json); + } + catch (JsonReaderException) + { + // JSON不正 + return false; + } + + // 3) ピンポイント取得 + // 例: "app.auth.password" / "items[0].name" / "users['alice'].age" 等 + try + { + var token = jo.SelectToken(jsonPath); + if (token is null) + { + return false; // キー未存在 + } + + value = token; // JValue/JObject/JArray いずれもOK + return true; + } + catch (ArgumentException) + { + // JSONPathの書式誤り(SelectTokenが例外) + return false; + } + } + + /// + /// 文字列として取得(JToken型を意識したくない場合の軽量ヘルパー)。 + /// + public static bool TryGetJsonString(string path, string jsonPath, out string result) + { + result = null; + if (!TryGetJsonKey(path, jsonPath, out var token)) return false; + + // JValue以外(オブジェクト/配列)の場合は ToString() を許容するか判断(ここではJValueのみ厳密) + if (token is JValue jv) + { + // 文字列ならそのまま + if (jv.Type == JTokenType.String) + { + result = (string)jv.Value; + return true; + } + + // 数値・真偽・Null などは ToString() で返す設計でもOK + // ここでは文字列のみ厳密に true とします(お好みで分岐調整) + return false; + } + return false; + } + + /// + /// 符号付き整数(Int64)で取得(intの範囲超えも想定)。 + /// + public static bool TryGetJsonInt64(string path, string jsonPath, out long result) + { + result = default; + if (!TryGetJsonKey(path, jsonPath, out var token)) return false; + + if (token is JValue jv) + { + // JValueが数値型なら long へ + if (jv.Type == JTokenType.Integer) + { + // Newtonsoftは内部的に long を持つのでキャスト可能 + try + { + result = Convert.ToInt64(jv.Value); + return true; + } + catch + { + return false; + } + } + + // 文字列数値を許可する場合("42" → 42) + if (jv.Type == JTokenType.String && long.TryParse((string)jv.Value, out var parsed)) + { + result = parsed; + return true; + } + } + return false; + } + + /// + /// 任意型の既定値付き取得(存在しなければ defaultValue を返す)。 + /// 例:GetJsonKeyOrDefault(path, "app.auth.retries", 3) + /// + public static T GetJsonKeyOrDefault(string path, string jsonPath, T defaultValue) + { + if (!TryGetJsonKey(path, jsonPath, out var token) || token is null) + return defaultValue; + + try + { + // JToken → 任意型に変換(失敗時は既定値) + var val = token.ToObject(); + return val == null ? defaultValue : val; + } + catch + { + return defaultValue; + } + } + } +} diff --git a/IO/File/PathUxer.cs b/IO/File/PathUxer.cs new file mode 100644 index 0000000..f92619d --- /dev/null +++ b/IO/File/PathUxer.cs @@ -0,0 +1,174 @@ +using KssSmaPlaLib.Commons; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.IO.File +{ + public static class PathUxer + { + public static string CreateFullPathEx( + string folderPath, + string fileName, + string dtPrefix = "", + string dtSuffix = "") + { + DateTime now = DateTime.Now; + + // フォルダ:未指定時はカレントフォルダ + folderPath = string.IsNullOrEmpty(folderPath) ? "." : folderPath; + + // ファイル名:未指定時は"noname.txt"とする + fileName = string.IsNullOrEmpty(fileName) ? "noname.txt" : fileName; + + if (fileName.IndexOf('.') == -1) + { + // 拡張子がないとき、付与する + fileName += ".txt"; + } + + // ファイルパス + var path = Path.Combine( + Path.GetFullPath(folderPath), + (string.IsNullOrEmpty(dtPrefix) ? "" : now.ToString(dtPrefix)) + + fileName.Split('.')[0] + + (string.IsNullOrEmpty(dtSuffix) ? "" : now.ToString(dtSuffix)) + + "." + + fileName.Split('.')[1]); + + return path; + } + + public static string CombineUnixPath(params string[] parts) + { + return string.Join("/", parts.Select(p => p.Trim('/'))); + } + + + /// + /// ファイル名(パス不可)を受け取り、存在すればフルパスを返す。 + /// 探索順:①このDLLのフォルダ → ②エントリEXEのフォルダ → ③AppDomain.BaseDirectory + /// (C)昼コパソフトウェア㈱ + /// + /// パスではないファイル名のみ(例: "config.json")。 + /// 見つかったファイルのフルパス。未発見時は null。 + /// 発見した場合 true、未発見の場合 false。 + public static bool TryResolveInAssemblyFolder(string fileName, out string fullPath) + { + fullPath = null; + + // --- ガード:パスを排除(「ファイル名のみ」の前提を厳守) --- + if (string.IsNullOrWhiteSpace(fileName)) + return false; + + // ディレクトリ成分が入っていたら拒否 + if (!string.Equals(fileName, Path.GetFileName(fileName), StringComparison.Ordinal)) + return false; + + // 予約文字の簡易チェック(Windows前提の一般的なNG文字) + foreach (var ch in Path.GetInvalidFileNameChars()) + { + if (fileName.IndexOf(ch) >= 0) + return false; + } + + // --- ① このメソッドを含むライブラリ(DLL)のフォルダ --- + var dllFolder = SafeGetDirectoryName(SafeGetAssemblyLocation(Assembly.GetExecutingAssembly())); + if (TryCombineAndExists(dllFolder, fileName, out fullPath)) + return true; + + // --- ② エントリアセンブリ(EXE)のフォルダ --- + string exeFolder = null; + var entry = Assembly.GetEntryAssembly(); + if (entry != null) + { + exeFolder = SafeGetDirectoryName(SafeGetAssemblyLocation(entry)); + } + else + { + // サービスや特殊ホストで EntryAssembly が null の場合のフォールバック + try + { + exeFolder = SafeGetDirectoryName(Process.GetCurrentProcess()?.MainModule?.FileName); + } + catch + { + // 取れないケースもあるので無理せず次へ + exeFolder = null; + } + } + + if (TryCombineAndExists(exeFolder, fileName, out fullPath)) + return true; + + // --- ③ AppDomain 基準(ClickOnce等のケースでも安定) --- + var baseDir = AppDomain.CurrentDomain?.BaseDirectory; + if (TryCombineAndExists(baseDir, fileName, out fullPath)) + return true; + + // --- 未発見 --- + fullPath = null; + return false; + } + + /// + /// 指定フォルダに fileName を結合し、存在すれば out に設定して true。 + /// + private static bool TryCombineAndExists(string folder, string fileName, out string path) + { + path = null; + if (string.IsNullOrWhiteSpace(folder)) + return false; + + try + { + var candidate = Path.Combine(folder, fileName); + if (System.IO.File.Exists(candidate)) + { + path = candidate; + return true; + } + } + catch + { + // パス結合や存在確認で例外が出てもスキップして未発見扱い + } + return false; + } + + /// + /// Assembly.Location を安全に取得(Reflection-only / 影響ケースを考慮)。 + /// + private static string SafeGetAssemblyLocation(Assembly asm) + { + try + { + return asm?.Location; + } + catch + { + return null; + } + } + + /// + /// DirectoryName を安全に取得。 + /// + private static string SafeGetDirectoryName(string path) + { + try + { + return string.IsNullOrEmpty(path) ? null : Path.GetDirectoryName(path); + } + catch + { + return null; + } + } + } +} diff --git a/InBox/IFilterUxer.cs b/InBox/IFilterUxer.cs new file mode 100644 index 0000000..6344e66 --- /dev/null +++ b/InBox/IFilterUxer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.InBox +{ + // インターフェース定義 + public interface IFilterUxer + { + // 単一:候補がフィルタを通過できるか? + bool CanPass(string candidate); + + // 複数:1件でも通過できるか?(ガード句向け) + bool HasAnyPass(IEnumerable candidates); + + // 複数:通過したものだけを返す(既存流儀) + IEnumerable ApplyFilter(IEnumerable candidates); + } +} diff --git a/InBox/LineFilterUxer.cs b/InBox/LineFilterUxer.cs new file mode 100644 index 0000000..18d1527 --- /dev/null +++ b/InBox/LineFilterUxer.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.InBox +{ + public class LineFilterUxer : IFilterUxer + { + private readonly HashSet includedLines; + private readonly HashSet excludedLines; + + public LineFilterUxer(IEnumerable included, IEnumerable excluded) + { + includedLines = new HashSet(included ?? Enumerable.Empty()); + excludedLines = new HashSet(excluded ?? Enumerable.Empty()); + } + + public IEnumerable ApplyFilter(IEnumerable lineNames) + { + if (includedLines.Any()) + { + // Included優先 → lineNamesの中でincludedに含まれるものだけ返す + return lineNames.Where(name => includedLines.Contains(name)); + } + + // Excluded適用 → lineNamesの中でexcludedに含まれないものだけ返す + return lineNames.Where(name => !excludedLines.Contains(name)); + } + + public bool CanPass(string lineName) + { + return HasAnyPass(new[] { lineName }); + } + + public bool HasAnyPass(IEnumerable candidates) + { + return ApplyFilter(candidates).Any(); + } + } +} diff --git a/InBox/SheetFilterUxer.cs b/InBox/SheetFilterUxer.cs new file mode 100644 index 0000000..50ced9c --- /dev/null +++ b/InBox/SheetFilterUxer.cs @@ -0,0 +1,72 @@ +using KssSmaPlaLib.Commons; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.InBox +{ + public class SheetFilterUxer : IFilterUxer + { + private readonly HashSet includedSheets; + private readonly HashSet excludedSheets; + + public SheetFilterUxer(IEnumerable included, IEnumerable excluded) + { + includedSheets = new HashSet(included ?? Enumerable.Empty()); + excludedSheets = new HashSet(excluded ?? Enumerable.Empty()); + } + + public IEnumerable ApplyFilter(IEnumerable sheetNames) + { + if (includedSheets.Any()) + { + // Included優先 → lineNamesの中でincludedに含まれるものだけ返す + //return sheetNames.Where(name => includedSheets.Contains(name)); + // ワイルドカード(*)ありとする! + List listA = new List(); + foreach (var name in sheetNames) + { + foreach (var iName in includedSheets) + { + if (StringUxer.WildcardMatch(name, iName)) + { + listA.Add(name); + break; + } + } + } + return listA; + } + + // Excluded適用 → lineNamesの中でexcludedに含まれないものだけ返す + //return sheetNames.Where(name => !excludedSheets.Contains(name)); + // ワイルドカード(*)ありとする! + List listB = new List(); + foreach (var name in sheetNames) + { + var isMatch = false; + foreach (var eName in excludedSheets) + { + if (StringUxer.WildcardMatch(name, eName)) + { + isMatch = true; + break; + } + } + if (!isMatch) listB.Add(name); + } + return listB; + } + public bool CanPass(string sheetName) + { + return HasAnyPass(new[] { sheetName }); + } + + public bool HasAnyPass(IEnumerable sheetNames) + { + return ApplyFilter(sheetNames).Any(); + } + } +} diff --git a/Ini/IniDto.cs b/Ini/IniDto.cs new file mode 100644 index 0000000..481a79e --- /dev/null +++ b/Ini/IniDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Ini +{ + public class IniDto + { + public string Section { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + } +} diff --git a/Ini/IniHelper.cs b/Ini/IniHelper.cs new file mode 100644 index 0000000..f5a7fe6 --- /dev/null +++ b/Ini/IniHelper.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Ini +{ + public static class IniHelper + { + public const string INI_PATH = @".\Settings.ini"; + + public static List IniList = new List(); + + // Win32 API宣言 + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern int GetPrivateProfileString( + string lpAppName, + string lpKeyName, + string lpDefault, + StringBuilder lpReturnedString, + int nSize, + string lpFileName); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern bool WritePrivateProfileString( + string lpAppName, + string lpKeyName, + string lpString, + string lpFileName); + + /// + /// INIファイルから値を読み込み(セクション+キー指定) + /// + public static string ReadValue(string iniPath, string section, string key, string defaultValue = "") + { + // もしINIリストにあったら、そこから返す + var value = IniList + .FirstOrDefault(m => m.Section == section && m.Key == key) + ?.Value; + if (value != null) return (string)value; + + if (string.IsNullOrEmpty(iniPath)) iniPath = INI_PATH; + var sb = new StringBuilder(1024); + GetPrivateProfileString(section, key, defaultValue, sb, sb.Capacity, iniPath); + var str = sb.ToString(); + str = string.IsNullOrEmpty(str) ? defaultValue : str; + return str; + } + + /// + /// bool値の読み込み + /// + /// + /// + /// + /// + /// + public static bool ReadBoolValue(string iniPath, string section, string key, bool defaultValue = false) + { + var str = ReadValue(iniPath, section, key, defaultValue.ToString()); + var boolValue = bool.TryParse(str, out var b) ? b : defaultValue; + return boolValue; + } + + /// + /// 日付値の読み込み + /// + public static DateTime ReadDateValue(string iniPath, string section, string key, DateTime? defaultDate = null) + { + var defDate = defaultDate ?? DateTime.MinValue; + + var str = ReadValue(iniPath, section, key, defDate.ToString("yyyy/MM/dd")); + var dateValue = DateTime.TryParse(str, out var d) ? d : defDate; + return dateValue; + } + + /// + /// INIファイルに値を書き込み(セクション+キー+値指定) + /// + public static bool WriteValue(string iniPath, string section, string key, string value) + { + if (string.IsNullOrEmpty(iniPath)) iniPath = INI_PATH; + return WritePrivateProfileString(section, key, value, iniPath); + } + + /// + /// bool値の書き込み + /// + /// + /// + /// + /// + /// + public static bool WriteBoolValue(string iniPath, string section, string key, bool value) + { + return WriteValue(iniPath, section, key, value.ToString()); + } + + /// + /// 日付値の書き込み + /// + /// + /// + /// + /// + /// + public static bool WriteDateValue(string iniPath, string section, string key, DateTime value) + { + return WriteValue(iniPath, section, key, value.ToString("yyyy/MM/dd")); + } + + public static string OpenIni(string initialFolderPath = "") + { + using (var dlg = new OpenFileDialog()) + { + dlg.Multiselect = false; + dlg.Title = "Settings.ini ファイルを選択してください。"; + dlg.AddExtension = true; + dlg.CheckFileExists = true; + dlg.CheckPathExists = true; + dlg.RestoreDirectory = true; + dlg.Filter = "設定ファイル(*.ini)|*.ini"; + if (!string.IsNullOrEmpty(initialFolderPath)) + { + // 最初に見せるフォルダ(デスクトップなど) + dlg.InitialDirectory = initialFolderPath; + } + + // ダイアログを表示して「OK」が押されたら処理 + if (dlg.ShowDialog() == DialogResult.Cancel) return string.Empty; + + // 選択されたファイルのパスを取得 + return dlg.FileName; + } + } + + public static object ReadValue(object value, string name, object iNI_KEY_ITAKUSAKI_MEI, string v) + { + throw new NotImplementedException(); + } + } +} diff --git a/Ini/IniReaderUxer.cs b/Ini/IniReaderUxer.cs new file mode 100644 index 0000000..fb57f16 --- /dev/null +++ b/Ini/IniReaderUxer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using KssSmaPlaLib.Commons; + +namespace KssSmaPlaLib.Ini +{ + /// + /// INIリーダー + /// + public static class IniReaderUxer + { + /// + /// 指定セクションのINI値読み込み + /// + /// INIパス + /// セクション + /// 指定のディクショナリ + public static Dictionary ReadSection(string path, string section) + { + var result = new Dictionary(); + var lines = File.ReadAllLines(path, System.Text.Encoding.GetEncoding("shift_jis")); + bool inSection = false; + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + // コメント行は飛ばす + if (trimmed.StartsWith(";")) continue; + + if (trimmed.StartsWith("[") && trimmed.EndsWith("]")) + inSection = trimmed == $"[{section}]"; + else if (inSection && trimmed.Contains("=")) + { + var index = trimmed.IndexOf('='); + if (index > 0) + { + var key = trimmed.Substring(0, index).Trim(); + var value = trimmed.Substring(index + 1).Trim(); + result[key] = value; + } + } + } + return result; + } + public static bool IsEmptyByIniDic(Dictionary iniDic,string key) + { + return string.IsNullOrEmpty(DictionaryUxer.GetValueOrDefault(iniDic, key, string.Empty)); + } + } +} diff --git a/Ini/IniReferenceResolveUxer.cs b/Ini/IniReferenceResolveUxer.cs new file mode 100644 index 0000000..cfe0fe0 --- /dev/null +++ b/Ini/IniReferenceResolveUxer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Ini +{ + public static class IniReferenceResolveUxer + { + public static Dictionary ReadSectionWithReference(string path, string section, int maxDepth = 3) + { + var allLines = File.ReadAllLines(path, System.Text.Encoding.GetEncoding("shift_jis")); + var allSections = ParseAllSections(allLines); + var result = new Dictionary(); + + if (!allSections.ContainsKey(section)) return result; + + foreach (var kvp in allSections[section]) + { + result[kvp.Key] = ResolveValue(allSections, kvp.Value, maxDepth); + } + + return result; + } + + private static Dictionary> ParseAllSections(string[] lines) + { + var sections = new Dictionary>(); + string currentSection = null; + + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith(";") || string.IsNullOrEmpty(trimmed)) continue; + + if (trimmed.StartsWith("[") && trimmed.EndsWith("]")) + { + currentSection = trimmed.Substring(1, trimmed.Length - 2); + sections[currentSection] = new Dictionary(); + } + else if (currentSection != null && trimmed.Contains("=")) + { + var index = trimmed.IndexOf('='); + var key = trimmed.Substring(0, index).Trim(); + var value = trimmed.Substring(index + 1).Trim(); + sections[currentSection][key] = value; + } + } + + return sections; + } + + private static string ResolveValue(Dictionary> sections, string value, int depth) + { + if (depth <= 0 || string.IsNullOrEmpty(value)) return value; + + var match = System.Text.RegularExpressions.Regex.Match(value, @"^\{\[(.+?)\](.+?)\}$"); + if (match.Success) + { + var refSection = match.Groups[1].Value; + var refKey = match.Groups[2].Value; + + if (sections.ContainsKey(refSection) && sections[refSection].ContainsKey(refKey)) + { + var nextValue = sections[refSection][refKey]; + return ResolveValue(sections, nextValue, depth - 1); + } + } + + return value; + } + } +} diff --git a/Input/Gamen/NumericInputForm.Designer.cs b/Input/Gamen/NumericInputForm.Designer.cs new file mode 100644 index 0000000..413736d --- /dev/null +++ b/Input/Gamen/NumericInputForm.Designer.cs @@ -0,0 +1,106 @@ +namespace KssSmaPlaLib.Input.Gamen +{ + partial class NumericInputForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.CancelButton2 = new System.Windows.Forms.Button(); + this.OkButton = new System.Windows.Forms.Button(); + this.NumericUpDownNumeric = new System.Windows.Forms.NumericUpDown(); + ((System.ComponentModel.ISupportInitialize)(this.NumericUpDownNumeric)).BeginInit(); + this.SuspendLayout(); + // + // CancelButton2 + // + this.CancelButton2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelButton2.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelButton2.Location = new System.Drawing.Point(255, 50); + this.CancelButton2.Name = "CancelButton2"; + this.CancelButton2.Size = new System.Drawing.Size(170, 53); + this.CancelButton2.TabIndex = 2; + this.CancelButton2.Text = "キャンセル"; + this.CancelButton2.UseVisualStyleBackColor = true; + // + // OkButton + // + this.OkButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.OkButton.Location = new System.Drawing.Point(79, 50); + this.OkButton.Name = "OkButton"; + this.OkButton.Size = new System.Drawing.Size(170, 53); + this.OkButton.TabIndex = 1; + this.OkButton.Text = "OK"; + this.OkButton.UseVisualStyleBackColor = true; + this.OkButton.Click += new System.EventHandler(this.OkButton_Click); + // + // NumericUpDownNumeric + // + this.NumericUpDownNumeric.Dock = System.Windows.Forms.DockStyle.Fill; + this.NumericUpDownNumeric.Location = new System.Drawing.Point(0, 0); + this.NumericUpDownNumeric.Maximum = new decimal(new int[] { + -727379969, + 232, + 0, + 0}); + this.NumericUpDownNumeric.Minimum = new decimal(new int[] { + 1316134911, + 2328, + 0, + -2147483648}); + this.NumericUpDownNumeric.Name = "NumericUpDownNumeric"; + this.NumericUpDownNumeric.Size = new System.Drawing.Size(437, 49); + this.NumericUpDownNumeric.TabIndex = 0; + this.NumericUpDownNumeric.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; + // + // NumericInputForm + // + this.AcceptButton = this.OkButton; + this.AutoScaleDimensions = new System.Drawing.SizeF(19F, 42F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.CancelButton2; + this.ClientSize = new System.Drawing.Size(437, 115); + this.Controls.Add(this.NumericUpDownNumeric); + this.Controls.Add(this.OkButton); + this.Controls.Add(this.CancelButton2); + this.Font = new System.Drawing.Font("メイリオ", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(128))); + this.Margin = new System.Windows.Forms.Padding(6, 7, 6, 7); + this.Name = "NumericInputForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "数値入力"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.NumericInputForm_FormClosing); + this.Load += new System.EventHandler(this.NumericInputForm_Load); + ((System.ComponentModel.ISupportInitialize)(this.NumericUpDownNumeric)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.Button CancelButton2; + private System.Windows.Forms.Button OkButton; + private System.Windows.Forms.NumericUpDown NumericUpDownNumeric; + } +} \ No newline at end of file diff --git a/Input/Gamen/NumericInputForm.cs b/Input/Gamen/NumericInputForm.cs new file mode 100644 index 0000000..c7dd021 --- /dev/null +++ b/Input/Gamen/NumericInputForm.cs @@ -0,0 +1,41 @@ +using KssSmaPlaLib.Forms; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Data.Entity.Core.Mapping; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Input.Gamen +{ + public partial class NumericInputForm : Form + { + public decimal NumericNumeric { get; set; } = 0; + + public NumericInputForm(decimal numeric) + { + InitializeComponent(); + + NumericUpDownNumeric.Value = numeric; + } + + private void NumericInputForm_Load(object sender, EventArgs e) + { + FormHelper.LoadFormBounds(this); + } + + private void NumericInputForm_FormClosing(object sender, FormClosingEventArgs e) + { + FormHelper.SaveFormBounds(this); + } + + private void OkButton_Click(object sender, EventArgs e) + { + NumericNumeric = NumericUpDownNumeric.Value; + } + } +} diff --git a/Input/Gamen/NumericInputForm.resx b/Input/Gamen/NumericInputForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Input/Gamen/NumericInputForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Input/Gamen/TextInputForm.Designer.cs b/Input/Gamen/TextInputForm.Designer.cs new file mode 100644 index 0000000..60e94c2 --- /dev/null +++ b/Input/Gamen/TextInputForm.Designer.cs @@ -0,0 +1,95 @@ +namespace KssSmaPlaLib.Input.Gamen +{ + partial class TextInputForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.TextText = new System.Windows.Forms.TextBox(); + this.CancelButton2 = new System.Windows.Forms.Button(); + this.OkButton = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // TextText + // + this.TextText.Dock = System.Windows.Forms.DockStyle.Fill; + this.TextText.Location = new System.Drawing.Point(0, 0); + this.TextText.Name = "TextText"; + this.TextText.Size = new System.Drawing.Size(437, 49); + this.TextText.TabIndex = 0; + // + // CancelButton2 + // + this.CancelButton2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelButton2.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelButton2.Location = new System.Drawing.Point(255, 50); + this.CancelButton2.Name = "CancelButton2"; + this.CancelButton2.Size = new System.Drawing.Size(170, 53); + this.CancelButton2.TabIndex = 1; + this.CancelButton2.Text = "キャンセル"; + this.CancelButton2.UseVisualStyleBackColor = true; + // + // OkButton + // + this.OkButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.OkButton.Location = new System.Drawing.Point(79, 50); + this.OkButton.Name = "OkButton"; + this.OkButton.Size = new System.Drawing.Size(170, 53); + this.OkButton.TabIndex = 2; + this.OkButton.Text = "OK"; + this.OkButton.UseVisualStyleBackColor = true; + this.OkButton.Click += new System.EventHandler(this.OkButton_Click); + // + // TextInputForm + // + this.AcceptButton = this.OkButton; + this.AutoScaleDimensions = new System.Drawing.SizeF(19F, 42F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.CancelButton2; + this.ClientSize = new System.Drawing.Size(437, 115); + this.Controls.Add(this.OkButton); + this.Controls.Add(this.CancelButton2); + this.Controls.Add(this.TextText); + this.Font = new System.Drawing.Font("メイリオ", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(128))); + this.Margin = new System.Windows.Forms.Padding(6, 7, 6, 7); + this.Name = "TextInputForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "テキスト入力"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.TextInputForm_FormClosing); + this.Load += new System.EventHandler(this.TextInputForm_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TextBox TextText; + private System.Windows.Forms.Button CancelButton2; + private System.Windows.Forms.Button OkButton; + } +} \ No newline at end of file diff --git a/Input/Gamen/TextInputForm.cs b/Input/Gamen/TextInputForm.cs new file mode 100644 index 0000000..79aa73c --- /dev/null +++ b/Input/Gamen/TextInputForm.cs @@ -0,0 +1,42 @@ +using KssSmaPlaLib.Forms; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Data.Entity.Core.Mapping; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace KssSmaPlaLib.Input.Gamen +{ + public partial class TextInputForm : Form + { + public string EditedText { get; set; } = string.Empty; + + public TextInputForm(string text, char passwordChar = (char)0) + { + InitializeComponent(); + + TextText.PasswordChar = passwordChar; + TextText.Text = text; + } + + private void TextInputForm_Load(object sender, EventArgs e) + { + FormHelper.LoadFormBounds(this); + } + + private void TextInputForm_FormClosing(object sender, FormClosingEventArgs e) + { + FormHelper.SaveFormBounds(this); + } + + private void OkButton_Click(object sender, EventArgs e) + { + EditedText = TextText.Text; + } + } +} diff --git a/Input/Gamen/TextInputForm.resx b/Input/Gamen/TextInputForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Input/Gamen/TextInputForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/KssSmaPlaLib.csproj b/KssSmaPlaLib.csproj new file mode 100644 index 0000000..a2e04eb --- /dev/null +++ b/KssSmaPlaLib.csproj @@ -0,0 +1,233 @@ + + + + + + Debug + AnyCPU + {4136CE96-10AA-4B55-B49F-68C80E9E3BC8} + Library + Properties + KssSmaPlaLib + KssSmaPlaLib + v4.8 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + ラッキイ青透過版(せり様).ico + + + + + + + + + + + + + + + + + + + + + Form + + + + + + + + + + + + + + Form + + + DgvColumnEditorForm.cs + + + + Component + + + Form + + + + + + + + + + + + + + + + + + + + NumericInputForm.cs + + + Form + + + Form + + + TextInputForm.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AlertForm.cs + + + DgvColumnEditorForm.cs + + + PoiDgv.cs + + + NumericInputForm.cs + + + TextInputForm.cs + + + + + 0.105.0 + + + 6.5.1 + + + 3.1.1 + + + 7.1.3 + + + 10.3.0 + + + 13.0.4 + + + 4.1.8 + + + 4.3.0 + + + 6.0.0 + + + 7.0.0 + + + 2025.0.0 + + + + + + + + + + このプロジェクトは、このコンピューター上にない NuGet パッケージを参照しています。それらのパッケージをダウンロードするには、[NuGet パッケージの復元] を使用します。詳細については、http://go.microsoft.com/fwlink/?LinkID=322105 を参照してください。見つからないファイルは {0} です。 + + + + + + + \ No newline at end of file diff --git a/KssSmaPlaLib.sln b/KssSmaPlaLib.sln new file mode 100644 index 0000000..4793086 --- /dev/null +++ b/KssSmaPlaLib.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36401.2 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KssSmaPlaLib", "KssSmaPlaLib.csproj", "{4136CE96-10AA-4B55-B49F-68C80E9E3BC8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4136CE96-10AA-4B55-B49F-68C80E9E3BC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4136CE96-10AA-4B55-B49F-68C80E9E3BC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4136CE96-10AA-4B55-B49F-68C80E9E3BC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4136CE96-10AA-4B55-B49F-68C80E9E3BC8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ABA22849-DFFB-4FFD-BCAF-E55D748F155A} + EndGlobalSection +EndGlobal diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..73a7943 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// アセンブリに関する一般情報は以下を通して制御されます +// 制御されます。アセンブリに関連付けられている情報を変更するには、 +// これらの属性値を変更してください。 +[assembly: AssemblyTitle("KssSmaPlaLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("本田技研工業株式会社")] +[assembly: AssemblyProduct("KssSmaPlaLib")] +[assembly: AssemblyCopyright("Copyright © 本田技研工業株式会社 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// ComVisible を false に設定すると、このアセンブリ内の型は COM コンポーネントから +// 参照できなくなります。COM からこのアセンブリ内の型にアクセスする必要がある場合は、 +// その型の ComVisible 属性を true に設定してください。 +[assembly: ComVisible(false)] + +// このプロジェクトが COM に公開される場合、次の GUID が typelib の ID になります +[assembly: Guid("4136ce96-10aa-4b55-b49f-68c80e9e3bc8")] + +// アセンブリのバージョン情報は、以下の 4 つの値で構成されています: +// +// メジャー バージョン +// マイナー バージョン +// ビルド番号 +// リビジョン +// +[assembly: AssemblyVersion("1.0.8.0")] +[assembly: AssemblyFileVersion("1.0.8.0")] +// りれき💛 +// 1.0.3.0 INIファイルパスをEXEパスにするためPathUxerにメソッド追加 +// 1.0.4.0 ExcelToCsvToDb:新Excel書式対応 +// 1.0.5.0 アラート画面:属性名変更メニューを追加 +// 1.0.6.0 アラート画面:強烈モード調整 +// 1.0.7.0 列編集画面のインデックス不正不具合修正 +// 1.0.8.0 溶接ツール:新共通:DBの挿入前削除で対象データの再過去日から削除するよう修正 diff --git a/Tools/ClaraHack/NamespaceAdjust/NamespaceAdjusterUxer.cs b/Tools/ClaraHack/NamespaceAdjust/NamespaceAdjusterUxer.cs new file mode 100644 index 0000000..3170fd7 --- /dev/null +++ b/Tools/ClaraHack/NamespaceAdjust/NamespaceAdjusterUxer.cs @@ -0,0 +1,95 @@ +using KssSmaPlaLib.Encryption; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Tools.ClaraHack.NamespaceAdjust +{ + public static class NamespaceAdjusterUxer + { + public static bool TuckIntoMain(string[] args) + { + if (args.Length != 3) return false; + + if (args[0].ToLower() != "/namespaceadjuster") return false; + + var projectTopFolderPath = args[1]; + var projectName = args[2]; + + if (NamespaceAdjusterUxer.Try( + projectTopFolderPath, + projectName, + out var updatedCount, + out var resultMessage) == false) + { + Console.WriteLine(resultMessage); + } + else + { + Console.WriteLine($"namespace調整完了。[更新ファイル数:{updatedCount}]"); + } + return true; + } + + public static bool Try( + string projectRootPath, + string projectName, + out int updatedCount, + out string resultMessage) + { + updatedCount = 0; + resultMessage = string.Empty; + + // フォルダバリデ + if (!Directory.Exists(projectRootPath)) + { + resultMessage = $"プロジェクトルートフォルダが存在しません。[プロジェクトルートフォルダ:{projectRootPath}]"; + return false; + } + + // 配下全どり + var csFiles = Directory.GetFiles( + projectRootPath, + "*.cs", + SearchOption.AllDirectories); + + foreach (var filePath in csFiles) + { + // トップ下相対フォルダパス + var relativeDir = Path.GetDirectoryName(filePath)?.Substring(projectRootPath.Length).TrimStart(Path.DirectorySeparatorChar); + + // セパレータ変換('\'→'.') + var namespaceSuffix = relativeDir?.Replace(Path.DirectorySeparatorChar, '.'); + + // トップノード(PJ名)つけてフル化 + var fullNamespace = string.IsNullOrEmpty(namespaceSuffix) + // PJ名のみ(カレントはトップ下) + ? projectName + // PJ名+配下 + : $"{projectName}.{namespaceSuffix}"; + + // フル読み + var content = File.ReadAllText(filePath); + + // namespace行の置換 + var newContent = Regex.Replace( + content, + @"namespace\s+[\w\.]+", + $"namespace {fullNamespace}"); + + if (newContent != content) + { + // 内容変わった時だけ書き込み + File.WriteAllText(filePath, newContent); + updatedCount++; + } + } + + return true; + } + } +} diff --git a/Tools/FileHack/MergeSplitFilesUxer.cs b/Tools/FileHack/MergeSplitFilesUxer.cs new file mode 100644 index 0000000..47b2591 --- /dev/null +++ b/Tools/FileHack/MergeSplitFilesUxer.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Tools.FileHack +{ + public static class MergeSplitFilesUxer + { + public static bool TuckIntoMain(string[] args) + { + if (args.Length != 2) return false; + + if (args[0].ToLower() != "/filemerge") return false; + + var path = args[1]; + + if (!Try(path, out var messageForMe)) + { + Console.WriteLine($"ファイル結合に失敗しました。[エラー内容:{messageForMe}]"); + return false; + } + Console.WriteLine($"ファイル結合が成功しました。"); + return true; + } + + public static bool Try(string baseFilePath, out string resultMessage) + { + try + { + string folder = Path.GetDirectoryName(baseFilePath); + string fileName = Path.GetFileName(baseFilePath); + string outputPath = Path.Combine(folder, fileName); + + int partNum = 1; + using (var output = new FileStream(outputPath, FileMode.Create, FileAccess.Write)) + { + while (true) + { + string partPath = Path.Combine(folder, $"{fileName}.{partNum:D3}"); + if (!File.Exists(partPath)) break; + + using (var input = new FileStream(partPath, FileMode.Open, FileAccess.Read)) + { + input.CopyTo(output); + } + partNum++; + } + } + + resultMessage = $"結合成功:{partNum - 1}個のパートを結合しました。"; + return true; + } + catch (Exception ex) + { + resultMessage = $"結合失敗: {ex.Message}"; + return false; + } + } + } +} diff --git a/Tools/FileHack/SplitFileBySizeUxer.cs b/Tools/FileHack/SplitFileBySizeUxer.cs new file mode 100644 index 0000000..81b0a5c --- /dev/null +++ b/Tools/FileHack/SplitFileBySizeUxer.cs @@ -0,0 +1,69 @@ +using KssSmaPlaLib.Tools.ClaraHack.NamespaceAdjust; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KssSmaPlaLib.Tools.FileHack +{ + public static class SplitFileBySizeUxer + { + public static bool TuckIntoMain(string[] args) + { + if (args.Length != 3) return false; + + if (args[0].ToLower() != "/filesplit") return false; + + var path = args[1]; + if (!int.TryParse(args[2], out var kb)) + { + Console.WriteLine($"分割サイズ(KB)が数値でありません。[分割サイズ:{args[2]}]"); + return false; + } + + if (!Try(path, kb, out var messageForMe)) + { + Console.WriteLine($"ファイル分割に失敗しました。[エラー内容:{messageForMe}]"); + return false; + } + Console.WriteLine($"ファイル分割が成功しました。"); + return true; + } + + public static bool Try(string filePath, int sizeKB, out string resultMessage) + { + try + { + int partSize = sizeKB * 1024; + string folder = Path.GetDirectoryName(filePath); + string fileName = Path.GetFileName(filePath); + + int partNum = 1; + using (var input = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + byte[] buffer = new byte[partSize]; + int bytesRead; + + while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0) + { + string partPath = Path.Combine(folder, $"{fileName}.{partNum:D3}"); + using (var output = new FileStream(partPath, FileMode.Create, FileAccess.Write)) + { + output.Write(buffer, 0, bytesRead); + partNum++; + } + } + } + resultMessage = $"分割成功:{partNum - 1}個のパートに分割されました。"; + return true; + } + catch (Exception ex) + { + resultMessage = $"分割失敗: {ex.Message}"; + return false; + } + } + } +} diff --git a/app.config b/app.config new file mode 100644 index 0000000..050af3f --- /dev/null +++ b/app.config @@ -0,0 +1,58 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ラッキイ青透過版(せり様).ico b/ラッキイ青透過版(せり様).ico new file mode 100644 index 0000000000000000000000000000000000000000..3554ad987163e221f3f3a0179d6cbe11eb4c0f4a GIT binary patch literal 233150 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%0Am`tNC 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 YVx3+MmfQNBX1~&`PuZTVB~udq3zk`)j{pDw literal 0 HcmV?d00001