午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、BitmapSourceを含んだクラスをシリアライズ、と言うかファイルに保存と読み込みできた

シリアライズしたいクラス

クラス 名前 プロパティ1名前 プロパティ1型
基底クラス Data X double
派生クラス DataGroup Datas Collection
派生クラス DataTextBlock Text String
派生クラス DataImage ImageSource BitmapSource

この内のどれか単体をシリアライズしたり、階層構造になったDataGroup

        //DataGroup_Root
        //  ┣DataImage
        //  ┣DataTextBlock
        //  ┣DataImage
        //  ┗DataGroup
        //      ┗DataTextBlock


シリアライズって言ってるけど、今回のも実際にはシリアライズじゃない!
要はアプリの状態をファイルに保存しておいて、次回起動時とかに読み込んで状態を再現できればいいので、シリアライズかどうかはどうでもいい
ってことで、Bitmapは画像ファイルに変換、それ以外は普通にxml形式でシリアライズ、これらをzipファイルにまとめて保存する
この方法は前回にも使ったもので、前回と違うのは保存したいクラスにBitmapがあったりなかったりが入り混じっているところ
今回で言うDataImageクラスだけなら、前回の方法でできる

Data Bitmap
DataImage1 Bitmap1
DataImage2 Bitmap2
DataImage3 Bitmap3

これなら連番にでもしておいて、読み込むときに順番に指定するだけでよかった
けど、今回は入り混じって階層構造もあるので

Data Bitmap
DataImage1 Bitmap1
DataTextBlock1
DataImage2 Bitmap2

これだと読み込みの際に、DataTextBlock1にBitmap2を指定することになる
んー、今思ったけど、DataImageだった場合だけBitmapを指定するって処理でもできたかも?

今回の方法は

Data Bitmap ID 保存ファイル名
DataImage1 Bitmap1 ID1 ID1.png
DataTextBlock1
DataImage2 Bitmap2 ID2 ID2.png

DataImageクラスにはString型のプロパティIDを付けておいて、BitmapはIDの名前で保存、読み込み時にIDで対応する画像を探して指定することにした
IDは重複されては困るので、Guidってのを使ってみた

learn.microsoft.com

www.weblio.jp

GUIDは、絶対的に一意であることが保証されてるわけではないが、実用上はほぼ、世界中に唯一とみなして扱って困難がないといわれている。

かっこいい


コード

2023WPF/20230114_SerializeWithBitmap/20230114_SerializeWithBitmap at main · gogowaten/2023WPF

github.com

Data.cs

using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Windows.Media.Imaging;

namespace _20230114_SerializeWithBitmap
{
    
    // 型の識別用に用意したけど今回は未使用    
    public enum TType { None = 0, TextBlock, Group, Image, Rectangle }


    [KnownType(typeof(DataImage)), KnownType(typeof(DataGroup)), KnownType(typeof(DataTextBlock))]
    public abstract class Data : IExtensibleDataObject
    {
        public ExtensionDataObject? ExtensionData { get; set; }

        public double X;
        public double Y;
        public TType Type { get; protected set; } = TType.None;
    }


    public class DataGroup : Data
    {
        public ObservableCollection<Data> Datas { get; set; } = new();
        public DataGroup() { Type = TType.Group; }
    }


    public class DataTextBlock : Data
    {
        public string? Text;
        public DataTextBlock() { Type = TType.TextBlock; }
    }

    public class DataImage : Data
    {
        //画像、それ自体は直接シリアライズしないので[IgnoreDataMember]
        [IgnoreDataMember] public BitmapSource? ImageSource;
        //シリアライズ時の画像ファイル名に使用、Guidで一意の名前作成している
        public string Guid { get; set; } = System.Guid.NewGuid().ToString();
        public DataImage() { Type = TType.Image; }
    }
}



MainWindow.xaml.cs

using System;
using System.IO.Compression;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Xml;

namespace _20230114_SerializeWithBitmap
{
    public partial class MainWindow : Window
    {
        private readonly string ZIP_FILE_PATH = "E:\\20230113.zip";
        private readonly string XML_FILE_NAME = "Data.xml";

        public MainWindow()
        {
            InitializeComponent();

            //DataImage、Bitmapを含むDataクラス
            DataImage dataImage1 = new()
            {
                X = 150,
                Y = 10,
                ImageSource = GetBitmapImage("D:\\ブログ用\\テスト用画像\\NEC_0541_2017_07_21_午後わてん__Matrix4x4_1.png")
            };

            //DataImage単体でセーブロード確認
            SaveToZip(ZIP_FILE_PATH, dataImage1);
            Data? DataImage単体確認用 = LoadFromZip(ZIP_FILE_PATH);


            //階層構造DataGroup、普通にシリアライズできる値とBitmapを混ぜたData
            //DataGroup_Root
            //  ┣DataImage
            //  ┣DataTextBlock
            //  ┣DataImage
            //  ┗DataGroup
            //      ┗DataTextBlock
            DataGroup dataGroupRoot = new() { X = 10, Y = 10 };
            dataGroupRoot.Datas.Add(dataImage1);
            dataGroupRoot.Datas.Add(new DataTextBlock()
            {
                X = 120,
                Y = 120,
                Text = "マヨネー樹の花の色"
            });
            dataGroupRoot.Datas.Add(new DataImage()
            {
                X = 30,
                Y = 140,
                ImageSource = GetBitmapImage($"D:\\ブログ用\\テスト用画像\\collection1.png")
            });
            DataGroup dataGroup1 = new() { X = 20, Y = 30 };
            dataGroup1.Datas.Add(new DataTextBlock() { X = 20, Y = 30, Text = "東方不敗の理を顕す" });
            dataGroupRoot.Datas.Add(dataGroup1);

            //階層構造Dataのセーブロード確認
            SaveToZip(ZIP_FILE_PATH, dataGroupRoot);
            Data? 階層構造Data確認用 = LoadFromZip(ZIP_FILE_PATH);

        }


        private BitmapImage GetBitmapImage(string path)
        {
            BitmapImage bitmap = new();
            FileStream stream = File.OpenRead(path);
            {
                bitmap.BeginInit();
                bitmap.StreamSource = stream;
                //bitmap.CacheOption = BitmapCacheOption.OnLoad;
                //bitmap.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
                bitmap.EndInit();
                //bitmap.Freeze();
            }
            return bitmap;
        }

        private void SaveToZip(string filePath, Data data)
        {
            try
            {
                using FileStream zipStream = File.Create(filePath);
                using (ZipArchive archive = new(zipStream, ZipArchiveMode.Create))
                {
                    //xml形式にシリアライズして、それをzipに詰め込む
                    ZipArchiveEntry entry = archive.CreateEntry(XML_FILE_NAME);
                    using (Stream entryStream = entry.Open())
                    {
                        XmlWriterSettings settings = new()
                        {
                            Indent = true,
                            Encoding = Encoding.UTF8,
                            NewLineOnAttributes = true,
                            ConformanceLevel = ConformanceLevel.Fragment,
                        };
                        //シリアライズする型は基底クラス型のDataで大丈夫
                        DataContractSerializer serializer = new(typeof(Data));
                        using var writer = XmlWriter.Create(entryStream, settings);
                        try { serializer.WriteObject(writer, data); }
                        catch (Exception ex) { MessageBox.Show(ex.Message); }
                    }
                    //png形式にした画像をzipに詰め込む
                    AAA(archive);
                }

            }
            catch (Exception ex) { MessageBox.Show(ex.Message); }

            void AAA(ZipArchive archive)
            {
                //if (data.Type == TType.Group)//こっちの方が速いかも
                if (data is DataGroup group)
                {
                    foreach (var item in group.Datas)
                    {
                        if (item is DataImage dImage)
                        {
                            BBB(dImage, archive);
                        }
                    }
                }
                else if (data is DataImage ddd)
                {
                    BBB(ddd, archive);
                }
            }
            void BBB(DataImage dImage, ZipArchive archive)
            {
                ZipArchiveEntry entry = archive.CreateEntry(dImage.Guid + ".png");
                using Stream entryStream = entry.Open();
                PngBitmapEncoder encoder = new();
                encoder.Frames.Add(BitmapFrame.Create(dImage.ImageSource));
                using MemoryStream memStream = new();
                encoder.Save(memStream);
                memStream.Position = 0;
                memStream.CopyTo(entryStream);
            }
        }


        private Data? LoadFromZip(string filePath)
        {
            try
            {
                using FileStream zipStream = File.OpenRead(filePath);
                using ZipArchive archive = new(zipStream, ZipArchiveMode.Read);
                ZipArchiveEntry? entry = archive.GetEntry(XML_FILE_NAME);
                if (entry != null)
                {
                    //デシリアライズ
                    using Stream entryStream = entry.Open();
                    DataContractSerializer serializer = new(typeof(Data));
                    using var reader = XmlReader.Create(entryStream);
                    Data? data = (Data?)serializer.ReadObject(reader);
                    if (data is null) return null;
                    //DataがDataImage型ならzipから画像を取り出して設定
                    Sub(data, archive);
                    return data;
                }
            }
            catch (Exception ex) { MessageBox.Show(ex.Message); }
            return null;

            void Sub(Data data, ZipArchive archive)
            {
                if (data is DataGroup group)
                {
                    foreach (Data item in group.Datas)
                    {
                        if (item is DataImage dImage)
                        {
                            SubSub(dImage, archive);
                        }
                    }
                }
                else if (data is DataImage dataImage)
                {
                    SubSub(dataImage, archive);
                }
            }
            void SubSub(DataImage data, ZipArchive archive)
            {
                //Guidに一致する画像ファイルをデコードしてプロパティに設定
                ZipArchiveEntry? imageEntry = archive.GetEntry(data.Guid + ".png");
                if (imageEntry != null)
                {
                    using Stream imageStream = imageEntry.Open();
                    PngBitmapDecoder decoder =
                        new(imageStream,
                        BitmapCreateOptions.None,
                        BitmapCacheOption.Default);
                    data.ImageSource = decoder.Frames[0];//設定
                }
            }
        }


    }
}

try catchはよくわかっていないから使い方間違っているかも
Streamもねえ、掴みどころがない感じでわかっていない
zipとかシリアライズの部分はコピペしたもので、お蔵入りになっているPixtack紫陽花2ndの932~1067行目あたり


使用した画像

8色の4ビット画像

"D:\ブログ用\テスト用画像\NEC_0541_2017_07_21_午後わてん__Matrix4x4_1.png"


普通の32ビット画像

D:\ブログ用\テスト用画像\collection1.png




確認

一時停止して確認
ファイルに保存したのを読み込んだところで一時停止して確認

DataImage単体

DataImage単体部分
この部分を保存して読み込んだ結果は
DataImage単体結果
画像が入っているDataImageのImageSourceプロパティをBitmapSourceVisualizerで確認したところ、期待通り!
画像以外のXとYの値も再現されている


階層構造

        //DataGroup_Root
        //  ┣DataImage
        //  ┣DataTextBlock
        //  ┣DataImage
        //  ┗DataGroup
        //      ┗DataTextBlock


この部分は

階層構造部分
階層構造はできてる
中身は?
階層構造中身
できてる!

2つの画像も
2つの画像確認
入っている


保存zipファイルの中身

保存されたzipファイル
これの中身は
zipファイルの中身
Bitmapはpng画像としてそれぞれ保存されていて、それ以外の値はxml形式で保存されている
Data.xamlテキストエディタで開いてみると
Data.xmlの中身



参照したところ

ZipFile、ZipArchiveクラスを使用して、ZIP圧縮、展開(解凍)、リスト表示などを行う - .NET Tips (VB.NET,C#...) dobon.net

DataContractSerializerを使って、オブジェクトのXMLシリアル化、逆シリアル化を行う - .NET Tips (VB.NET,C#...) dobon.net いつもわかりやすい説明で助かる

DataContractJsonSerializerの詳細動作 - ごった日記 mokake.hatenablog.com まさに詳細!


感想

今回のでPixtack紫陽花3rdの主要なパーツはできたはず


関連記事

前回の方法は7年前…だと?
gogowaten.hatenablog.com

WPFのBitmapSourceVisualizerのダウンロード先と使い方は
gogowaten.hatenablog.com