Files
lserp_cs_6.0/插件库/Lskj.Control/Model/LabPicUrlPreview.cs
T
cyf 47cf35aa28 SVN r1104
SVN-Revision: r1104
2026-01-30 07:48:24 +00:00

578 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.cs C# 7.3 / DevExpress 15.2 兼容)
// - 公用方法封装在一个类里:InitPicColumn / Attach / AttachPreview
// - 线程安全缓存:缩略图 & 原图 分离缓存;并发限流 + LRU 清理
// - 只在同步阶段写 e.Value;异步完成后刷新单元格或更新预览窗体
// ============================================================================
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.XtraGrid;
using DevExpress.XtraGrid.Columns;
using DevExpress.XtraGrid.Views.Base;
using DevExpress.XtraGrid.Views.Grid;
using DevExpress.XtraGrid.Views.BandedGrid;
using DevExpress.XtraGrid.Views.Grid.ViewInfo;
using DevExpress.XtraGrid.Views.BandedGrid.ViewInfo;
using Lskj.Control.Model;
using Lskj.Control;
using Lskj.Business;
public static class LabPicUrlPreview
{
// ====== 对外:初始化图片列(创建 RepositoryItemPictureEdit、设为 Unbound ======
public static void InitPicColumn(GridControl gridControl, GridColumn gridColumn, GridColumnModel model)
{
if (gridControl == null || gridColumn == null || model == null) return;
gridColumn.AppearanceCell.Options.UseTextOptions = true;
gridColumn.AppearanceCell.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Center;
gridColumn.OptionsColumn.AllowEdit = false;
// 唯一列名(保持你原逻辑)
gridColumn.FieldName = string.Format("{0}{1}", model.FieldName, Guid.NewGuid());
//gridColumn.FieldName = model.FieldName;
gridColumn.Tag = model; // 事件里取模型
RepositoryItemPictureEdit pictureEdit = new RepositoryItemPictureEdit();
pictureEdit.ShowMenu = false;
pictureEdit.NullText = " ";
pictureEdit.SizeMode = PictureSizeMode.Zoom;
gridControl.RepositoryItems.Add(pictureEdit);
gridColumn.UnboundType = UnboundColumnType.Object;
gridColumn.ColumnEdit = pictureEdit;
}
// ====== 对外:绑定“取缩略图”的事件(只需调用一次) ======
public static void Attach(ColumnView view)
{
if (view == null) return;
view.CustomUnboundColumnData -= OnCustomUnboundColumnData;
view.CustomUnboundColumnData += OnCustomUnboundColumnData;
}
// ====== 对外:绑定“双击大图预览”的事件(只需调用一次) ======
public static void AttachPreview(ColumnView view)
{
if (view == null) return;
view.DoubleClick -= OnViewDoubleClick;
view.DoubleClick += OnViewDoubleClick;
}
// ===================== 缩略图事件实现 =====================
private static void OnCustomUnboundColumnData(object sender, CustomColumnDataEventArgs e)
{
ColumnView view = sender as ColumnView;
if (view == null) return;
GridColumnModel m = e.Column.Tag as GridColumnModel;
if (m == null || m.FieldType != ControlType.LabPicUrl || !e.IsGetData) return;
// 1) 用 ListSourceRowIndex → RowHandle(兼容 DevExpress 15.2
int listIndex = e.ListSourceRowIndex;
int rowHandle = view.GetRowHandle(listIndex);
// 2) 读取原始 URL(优先 ListSource;兜底 e.Row
string raw = null;
try
{
object val = view.GetListSourceRowCellValue(listIndex, m.FieldName);
raw = val == null ? null : Convert.ToString(val);
}
catch
{
DataRowView drv = e.Row as DataRowView;
if (drv != null && drv.Row != null && drv.Row.Table != null && drv.Row.Table.Columns.Contains(m.FieldName))
raw = Convert.ToString(drv.Row[m.FieldName]);
}
if (string.IsNullOrWhiteSpace(raw)) return;
// 3) 拼接完整 URL(相对路径 -> OA 根地址)
string url = ResolveFullUrl(raw);
// 4) 目标高(属性或方法都兼容;<=18 则用 30)
int targetHeight = NormalizeHeight(m);
// 5) 缓存命中:当次赋值(只在同步阶段写 e.Value)
Image img;
if (ImageCache.TryGetThumb(url, targetHeight, out img))
{
e.Value = img;
return;
}
// 6) 未命中:安排异步下载;完成后刷新该单元格(不要在异步线程里改 e.Value)
System.Windows.Forms.Control invoker = view.GridControl; // UI 回调对象
ImageCache.GetThumbAsync(url, targetHeight, invoker, delegate ()
{
RefreshCellCompat(view, rowHandle, e.Column);
});
}
// ===================== 双击预览(原图) =====================
private static void OnViewDoubleClick(object sender, EventArgs e)
{
ColumnView view = sender as ColumnView;
if (view == null || view.GridControl == null) return;
// 命中测试(GridView / BandedGridView 兼容)
GridColumn hitColumn = null;
int rowHandle = -1;
Point pt = view.GridControl.PointToClient(Control.MousePosition);
GridView gv = view as GridView;
if (gv != null)
{
GridHitInfo hi = gv.CalcHitInfo(pt);
if (hi == null || !hi.InRowCell || hi.Column == null) return;
hitColumn = hi.Column;
rowHandle = hi.RowHandle;
}
else
{
BandedGridView bgv = view as BandedGridView;
if (bgv == null) return;
BandedGridHitInfo hi2 = bgv.CalcHitInfo(pt);
if (hi2 == null || !hi2.InRowCell || hi2.Column == null) return;
hitColumn = hi2.Column;
rowHandle = hi2.RowHandle;
}
// 只处理 LabPicUrl 列
GridColumnModel m = hitColumn.Tag as GridColumnModel;
if (m == null || m.FieldType != ControlType.LabPicUrl) return;
// 取当前行的 URL(优先 RowHandle 取值)
string raw = null;
try
{
object val = view.GetRowCellValue(rowHandle, m.FieldName);
raw = val == null ? null : Convert.ToString(val);
}
catch
{
// 兜底:通过 ListSourceRowIndex
int listIndex = view.GetDataSourceRowIndex(rowHandle);
try
{
object val2 = view.GetListSourceRowCellValue(listIndex, m.FieldName);// "GM_leb_TuShi_ZS"
raw = val2 == null ? null : Convert.ToString(val2);
}
catch { }
}
if (string.IsNullOrWhiteSpace(raw)) return;
string url = ResolveFullUrl(raw);
// —— 打开你的预览窗体(先以缩略图占位) ——
Image thumb = null;
ImageCache.TryGetThumb(url, NormalizeHeight(m), out thumb);
FrmPicture frm = new FrmPicture();
try
{
// 这些属性基于你原有窗体接口
frm.PicUrlColumns = view.Columns
.Where(c => c.Tag is GridColumnModel cm && cm.FieldType == ControlType.LabPicUrl)
.ToList();
frm.FocusedRow = (view as GridView) != null ? (view as GridView).GetDataRow(rowHandle) : null;
frm.FocusedColumn = hitColumn;
frm.Text = "预览";
frm.Picture = thumb; // 先用缩略图(可能为 null
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();
}
}
// ====== 视图安全刷新:GridView/BandedGridView 定向刷新,其他降级为 RefreshData ======
private static void RefreshCellCompat(ColumnView view, int rowHandle, GridColumn column)
{
try
{
GridView gv = view as GridView;
if (gv != null)
{
if (rowHandle >= 0) gv.RefreshRowCell(rowHandle, column);
else gv.RefreshData();
return;
}
BandedGridView bgv = view as BandedGridView;
if (bgv != null)
{
if (rowHandle >= 0) bgv.RefreshRowCell(rowHandle, column);
else bgv.RefreshData();
return;
}
view.RefreshData();
}
catch
{
try { view.RefreshData(); } catch { }
}
}
// ====== 兼容“属性或无参方法”的高度读取(避免 m.LabPicUrlHeight 被当作方法组出错) ======
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;
}
// ====== 解析完整 URL:相对路径 -> SystemInfo.Instance.OAUrl + 相对 ======
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('/');
}
}
// ====== 图片缓存(缩略图 & 原图)—— WebClient + Dictionary + SemaphoreC# 7.3 兼容 ======
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))
{
// 为避免对流的依赖,先 Image.FromStream,再复制
using (Image img = Image.FromStream(ms))
{
return new B