Files
lserp_cs_6.0/插件库/Lskj.Control/Model/LabPicUrlPreview_TreeList.cs
T
2026-07-02 10:11:39 +00:00

582 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// LabPicUrlPreview_TreeList.cs C# 7.3 / DevExpress 15.2 兼容)
// - 适配 TreeListInitPicColumn / Attach / AttachPreview
// - 复用原线程安全缓存:缩略图 & 原图分离缓存、并发限流 + LRU 清理
// - 同步阶段写 e.Value,异步完成后刷新 TreeList 单元格或预览窗体
// ============================================================================
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using DevExpress.Data;
using DevExpress.XtraEditors.Controls;
using DevExpress.XtraEditors.Repository;
using DevExpress.XtraTreeList;
using DevExpress.XtraTreeList.Columns;
using DevExpress.XtraTreeList.Nodes;
using DevExpress.XtraTreeList.ViewInfo;
using Lskj.Control.Model;
using Lskj.Control;
using Lskj.Business;
public static class LabPicUrlPreview_TreeList
{
// ====== 对外:初始化图片列(创建 RepositoryItemPictureEdit、设为 Unbound ======
public static void InitPicColumn(TreeList treeList, TreeListColumn treeListColumn, GridColumnModel model)
{
if (treeList == null || treeListColumn == null || model == null) return;
treeListColumn.AppearanceCell.Options.UseTextOptions = true;
treeListColumn.AppearanceCell.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Center;
treeListColumn.OptionsColumn.AllowEdit = false;
// 唯一列名(保持原逻辑,避免列名重复)
treeListColumn.FieldName = string.Format("{0}{1}", model.FieldName, Guid.NewGuid());
treeListColumn.Tag = model; // 事件中获取列模型
RepositoryItemPictureEdit pictureEdit = new RepositoryItemPictureEdit();
pictureEdit.ShowMenu = false;
pictureEdit.NullText = " ";
pictureEdit.SizeMode = PictureSizeMode.Zoom;
treeList.RepositoryItems.Add(pictureEdit);
treeListColumn.UnboundType = DevExpress.XtraTreeList.Data.UnboundColumnType.Object;
treeListColumn.ColumnEdit = pictureEdit;
}
// ====== 对外:绑定“取缩略图”的事件(只需调用一次) ======
public static void Attach(TreeList treeList)
{
if (treeList == null) return;
treeList.CustomUnboundColumnData -= OnCustomUnboundColumnData;
treeList.CustomUnboundColumnData += OnCustomUnboundColumnData;
}
// ====== 对外:绑定“双击大图预览”的事件(只需调用一次) ======
public static void AttachPreview(TreeList treeList)
{
if (treeList == null) return;
treeList.DoubleClick -= OnTreeListDoubleClick;
treeList.DoubleClick += OnTreeListDoubleClick;
}
// ===================== 缩略图事件实现(核心改造) =====================
private static void OnCustomUnboundColumnData(object sender, TreeListCustomColumnDataEventArgs e)
{
TreeList treeList = sender as TreeList;
if (treeList == null) return;
GridColumnModel m = e.Column.Tag as GridColumnModel;
if (m == null || m.FieldType != ControlType.LabPicUrl || !e.IsGetData) return;
// 1) TreeList 用 Node 直接定位(替代 Grid 的 RowHandle/ListSourceRowIndex
TreeListNode node = e.Node;
if (node == null) return;
// 2) 读取原始 URL(优先 Node 数据,兜底数据源)
string raw = null;
try
{
// TreeList 直接从 Node 获取字段值(兼容绑定 DataTable/自定义对象)
object val = node.GetValue(m.FieldName);
raw = val == null ? null : Convert.ToString(val);
}
catch
{
// 兜底:从数据源(如 DataRow)获取
DataRow row = node.Tag as DataRow;
if (row != null && row.Table.Columns.Contains(m.FieldName))
raw = Convert.ToString(row[m.FieldName]);
}
if (string.IsNullOrWhiteSpace(raw)) return;
// 3) 拼接完整 URL(复用原逻辑)
string url = ResolveFullUrl(raw);
// 4) 目标高度(复用原逻辑)
int targetHeight = NormalizeHeight(m);
// 5) 缓存命中:同步赋值 e.Value
Image img;
if (ImageCache.TryGetThumb(url, targetHeight, out img))
{
e.Value = img;
return;
}
// 6) 未命中:异步下载,完成后刷新当前单元格(TreeList 刷新 API 改造)
System.Windows.Forms.Control invoker = treeList; // UI 回调对象
ImageCache.GetThumbAsync(url, targetHeight, invoker, delegate ()
{
RefreshCellCompat(treeList, node, e.Column);
});
}
// ===================== 双击预览(原图)(核心改造) =====================
private static void OnTreeListDoubleClick(object sender, EventArgs e)
{
TreeList treeList = sender as TreeList;
if (treeList == null) return;
// 1) TreeList 命中测试(替代 Grid 的 CalcHitInfo
TreeListHitInfo hitInfo = treeList.CalcHitInfo(treeList.PointToClient(Control.MousePosition));
//if (hitInfo == null || !hitInfo.InNodeCell || hitInfo.Column == null) return;
if (hitInfo == null || hitInfo.HitInfoType != HitInfoType.Cell || hitInfo.Node == null || hitInfo.Column == null) return;
TreeListNode hitNode = hitInfo.Node;
TreeListColumn hitColumn = hitInfo.Column;
// 2) 只处理 LabPicUrl 类型列
GridColumnModel m = hitColumn.Tag as GridColumnModel;
if (m == null || m.FieldType != ControlType.LabPicUrl) return;
// 3) 读取当前 Node 的 URLTreeList 专属取值逻辑)
string raw = null;
try
{
object val = hitNode.GetValue(m.FieldName);
raw = val == null ? null : Convert.ToString(val);
}
catch
{
// 兜底:从 Node.Tag(如 DataRow)获取
DataRow row = hitNode.Tag as DataRow;
if (row != null && row.Table.Columns.Contains(m.FieldName))
raw = Convert.ToString(row[m.FieldName]);
}
if (string.IsNullOrWhiteSpace(raw)) return;
string url = ResolveFullUrl(raw);
// 4) 打开预览窗体(复用原逻辑,适配 TreeList 数据)
Image thumb = null;
ImageCache.TryGetThumb(url, NormalizeHeight(m), out thumb);
FrmTreePicture frm = new FrmTreePicture();
try
{
// 适配 TreeList 列与节点数据(替换原 Grid 的 Column/Row
frm.PicUrlColumns = treeList.Columns.Where(c => c.Tag is GridColumnModel cm && cm.FieldType == ControlType.LabPicUrl).ToList(); // 兼容原窗体列集合类型
frm.FocusedRow = hitNode.Tag as DataRow; // 传递 Node 绑定的 DataRow
frm.FocusedColumn = hitColumn;
frm.Text = "预览";
frm.Picture = thumb;
frm.WindowState = FormWindowState.Maximized;
frm.StartPosition = FormStartPosition.CenterScreen;
// 异步拉取原图并更新(复用原缓存逻辑)
ImageCache.GetFullAsync(url, frm, delegate (Image full)
{
if (full == null) return;
try
{
if (!frm.IsDisposed) frm.Picture = full;
}
catch { }
});
frm.ShowDialog();
}
finally
{
if (frm != null && !frm.IsDisposed) frm.Dispose();
}
}
// ====== TreeList 单元格安全刷新(替代 Grid 的 RefreshRowCell ======
private static void RefreshCellCompat(TreeList treeList, TreeListNode node, TreeListColumn column)
{
try
{
if (treeList.IsDisposed || node == null || column == null) return;
// 整体刷新节点(避免单个单元格刷新失败)
if (!treeList.IsDisposed && node != null)
treeList.RefreshNode(node);
}
catch
{
try
{
// 刷新整个 TreeList(极端情况)
if (!treeList.IsDisposed)
treeList.RefreshDataSource();
}
catch
{
}
}
}
// ====== 以下方法完全复用原逻辑(无改造) ======
private static int NormalizeHeight(object model)
{
try
{
var t = model.GetType();
// 读取属性
var pi = t.GetProperty("LabPicUrlHeight");
if (pi != null)
{
int v = Convert.ToInt32(pi.GetValue(model, null));
return v <= 18 ? 30 : v;
}
// 读取无参方法
var mi = t.GetMethod("LabPicUrlHeight", Type.EmptyTypes);
if (mi != null)
{
int v = Convert.ToInt32(mi.Invoke(model, null));
return v <= 18 ? 30 : v;
}
}
catch { }
return 30;
}
private static string ResolveFullUrl(string raw)
{
if (string.IsNullOrEmpty(raw)) return raw;
if (raw.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return raw;
try
{
string oa = SystemInfo.Instance.OAUrl;
if (string.IsNullOrEmpty(oa)) return raw.TrimStart('/');
if (!oa.EndsWith("/")) oa += "/";
return oa + raw.TrimStart('/');
}
catch
{
return raw.TrimStart('/');
}
}
// ====== 图片缓存(完全复用原逻辑,无任何改造) ======
private static class ImageCache
{
// —— 公共接口(缩略图) ——
public static bool TryGetThumb(string url, int targetHeight, out Image img)
{
return ThumbCache.TryGet(url, targetHeight, out img);
}
public static void GetThumbAsync(string url, int targetHeight, System.Windows.Forms.Control invoker, Action onReady)
{
ThumbCache.GetOrCreateAsync(url, targetHeight, invoker, onReady);
}
// —— 公共接口(原图) ——
public static bool TryGetFull(string url, out Image img)
{
return FullCache.TryGet(url, out img);
}
public static void GetFullAsync(string url, System.Windows.Forms.Control invoker, Action<Image> onReady)
{
FullCache.GetOrCreateAsync(url, invoker, onReady);
}
// ===== 缩略图缓存实现 =====
private static class ThumbCache
{
private class CacheEntry
{
public Image Image;
public DateTime LastHitUtc;
}
private static readonly object _lock = new object();
private static readonly Dictionary<string, CacheEntry> _cache =
new Dictionary<string, CacheEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, Task<Image>> _tasks =
new Dictionary<string, Task<Image>>(StringComparer.OrdinalIgnoreCase);
private static readonly Semaphore _throttle = new Semaphore(4, 4); // 并发 4
private const int Capacity = 300;
static ThumbCache()
{
EnableTls12();
}
private static string MakeKey(string url, int h)
{
return url + "#h=" + h.ToString();
}
public static bool TryGet(string url, int targetHeight, out Image image)
{
string key = MakeKey(url, targetHeight);
lock (_lock)
{
CacheEntry e;
if (_cache.TryGetValue(key, out e) && e != null && e.Image != null)
{
e.LastHitUtc = DateTime.UtcNow;
_cache[key] = e;
image = e.Image;
return true;
}
}
image = null;
return false;
}
public static void GetOrCreateAsync(string url, int targetHeight, System.Windows.Forms.Control uiInvoker, Action onReady)
{
string key = MakeKey(url, targetHeight);
lock (_lock)
{
if (_cache.ContainsKey(key))
{
BeginInvokeSafe(uiInvoker, onReady);
return;
}
Task<Image> exist;
if (_tasks.TryGetValue(key, out exist))
{
exist.ContinueWith(delegate { BeginInvokeSafe(uiInvoker, onReady); }, TaskScheduler.Default);
return;
}
Task<Image> task = Task.Factory.StartNew<Image>(delegate
{
_throttle.WaitOne();
try
{
byte[] bytes = DownloadBytes(url);
if (bytes == null) return null;
using (Bitmap src = BytesToBitmap(bytes))
{
return ScaleBitmap(src, targetHeight);
}
}
catch { return null; }
finally { try { _throttle.Release(); } catch { } }
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).ContinueWith<Image>(delegate (Task<Image> t)
{
Image result = t.Result;
lock (_lock)
{
_tasks.Remove(key);
if (result != null)
{
_cache[key] = new CacheEntry { Image = result, LastHitUtc = DateTime.UtcNow };
TrimIfNeededUnsafe();
}
}
return result;
}, TaskScheduler.Default);
_tasks[key] = task;
task.ContinueWith(delegate { BeginInvokeSafe(uiInvoker, onReady); }, TaskScheduler.Default);
}
}
private static void TrimIfNeededUnsafe()
{
if (_cache.Count <= Capacity) return;
List<KeyValuePair<string, CacheEntry>> list = _cache.ToList();
list.Sort(delegate (KeyValuePair<string, CacheEntry> a, KeyValuePair<string, CacheEntry> b)
{
return a.Value.LastHitUtc.CompareTo(b.Value.LastHitUtc);
});
int removeCount = Math.Max(1, Capacity / 10);
for (int i = 0; i < removeCount && i < list.Count; i++)
{
string k = list[i].Key;
CacheEntry e;
if (_cache.TryGetValue(k, out e))
{
_cache.Remove(k);
try { if (e.Image != null) e.Image.Dispose(); } catch { }
}
}
}
}
// ===== 原图缓存实现(容量更小) =====
private static class FullCache
{
private class CacheEntry
{
public Image Image;
public DateTime LastHitUtc;
}
private static readonly object _lock = new object();
private static readonly Dictionary<string, CacheEntry> _cache =
new Dictionary<string, CacheEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, Task<Image>> _tasks =
new Dictionary<string, Task<Image>>(StringComparer.OrdinalIgnoreCase);
private static readonly Semaphore _throttle = new Semaphore(2, 2); // 原图并发更小
private const int Capacity = 50;
static FullCache()
{
EnableTls12();
}
public static bool TryGet(string url, out Image image)
{
lock (_lock)
{
CacheEntry e;
if (_cache.TryGetValue(url, out e) && e != null && e.Image != null)
{
e.LastHitUtc = DateTime.UtcNow;
_cache[url] = e;
image = e.Image;
return true;
}
}
image = null;
return false;
}
public static void GetOrCreateAsync(string url, System.Windows.Forms.Control uiInvoker, Action<Image> onReady)
{
lock (_lock)
{
Image cached;
if (TryGet(url, out cached))
{
BeginInvokeSafe(uiInvoker, delegate { onReady(cached); });
return;
}
Task<Image> exist;
if (_tasks.TryGetValue(url, out exist))
{
exist.ContinueWith(t => BeginInvokeSafe(uiInvoker, delegate { onReady(t.Result); }), TaskScheduler.Default);
return;
}
Task<Image> task = Task.Factory.StartNew<Image>(delegate
{
_throttle.WaitOne();
try
{
byte[] bytes = DownloadBytes(url);
if (bytes == null) return null;
using (Bitmap bmp = BytesToBitmap(bytes))
{
return CopyBitmap(bmp);
}
}
catch { return null; }
finally { try { _throttle.Release(); } catch { } }
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).ContinueWith<Image>(delegate (Task<Image> t)
{
Image result = t.Result;
lock (_lock)
{
_tasks.Remove(url);
if (result != null)
{
_cache[url] = new CacheEntry { Image = result, LastHitUtc = DateTime.UtcNow };
TrimIfNeededUnsafe();
}
}
return result;
}, TaskScheduler.Default);
_tasks[url] = task;
task.ContinueWith(t => BeginInvokeSafe(uiInvoker, delegate { onReady(t.Result); }), TaskScheduler.Default);
}
}
private static void TrimIfNeededUnsafe()
{
if (_cache.Count <= Capacity) return;
List<KeyValuePair<string, CacheEntry>> list = _cache.ToList();
list.Sort(delegate (KeyValuePair<string, CacheEntry> a, KeyValuePair<string, CacheEntry> b)
{
return a.Value.LastHitUtc.CompareTo(b.Value.LastHitUtc);
});
int removeCount = Math.Max(1, Capacity / 10);
for (int i = 0; i < removeCount && i < list.Count; i++)
{
string k = list[i].Key;
CacheEntry e;
if (_cache.TryGetValue(k, out e))
{
_cache.Remove(k);
try { if (e.Image != null) e.Image.Dispose(); } catch { }
}
}
}
}
// ===== 共享底层工具 =====
private static void EnableTls12()
{
try
{
const System.Security.Authentication.SslProtocols _Tls12 =
(System.Security.Authentication.SslProtocols)0x00000C00;
ServicePointManager.SecurityProtocol |= (SecurityProtocolType)_Tls12;
}
catch { }
}
private static byte[] DownloadBytes(string url)
{
using (WebClient wc = new WebClient())
{
return wc.DownloadData(url);
}
}
private static Bitmap BytesToBitmap(byte[] bytes)
{
using (MemoryStream ms = new MemoryStream(bytes))
{
using (Image img = Image.FromStream(ms))
{
return new Bitmap(img);
}
}
}
private static Image ScaleBitmap(Bitmap src, int targetHeight)
{
int h = targetHeight <= 0 ? 30 : targetHeight;
if (h < 18) h = 18;
double ratio = (double)h / (double)src.Height;
int w = (int)Math.Round((double)src.Width * ratio);
if (w < 1) w = 1;
return new Bitmap(src, new Size(w, h));
}
private static Image CopyBitmap(Bitmap src)
{
return new Bitmap(src);
}
private static void BeginInvokeSafe(System.Windows.Forms.Control c, Action action)
{
if (action == null) return;
try
{
if (c != null && !c.IsDisposed)
c.BeginInvoke(action);
else
action();
}
catch
{
try { action(); } catch { }
}
}
}
}