diff --git a/Documentation~/zh/changelog.md b/Documentation~/zh/changelog.md
index d7717f40..03c42c88 100644
--- a/Documentation~/zh/changelog.md
+++ b/Documentation~/zh/changelog.md
@@ -80,6 +80,7 @@ slug: /changelog
## master
+* (2026.02.26) 修复`SaveAsImage`被其他组件遮挡时无法正常保存的问题 (#337)
* (2026.02.26) 增加`Axis`的`mainAxis`参数设置主轴可控制柱图的朝向 (#331)
* (2026.02.03) 修复`UITable`的`viewport`在不同的锚点下可能会绘制异常的问题
* (2026.01.15) 修复`Pie`的点击有时候不响应的问题 (#357)
diff --git a/Editor/Charts/BaseChartEditor.cs b/Editor/Charts/BaseChartEditor.cs
index 739903b1..371c4e34 100644
--- a/Editor/Charts/BaseChartEditor.cs
+++ b/Editor/Charts/BaseChartEditor.cs
@@ -285,7 +285,7 @@ namespace XCharts.Editor
}
if (GUILayout.Button(Styles.btnSaveAsImage))
{
- m_Chart.SaveAsImage();
+ m_Chart.SaveAsImage("png", "", 4f);
}
if (m_CheckWarning)
{
diff --git a/Editor/MainComponents/UIComponentEditor.cs b/Editor/MainComponents/UIComponentEditor.cs
index b942a2bd..66609fe6 100644
--- a/Editor/MainComponents/UIComponentEditor.cs
+++ b/Editor/MainComponents/UIComponentEditor.cs
@@ -54,7 +54,7 @@ namespace XCharts.Editor
}
if (GUILayout.Button(Styles.btnSaveAsImage))
{
- m_UIComponent.SaveAsImage();
+ m_UIComponent.SaveAsImage("png", "", 4f);
}
OnDebugEndInspectorGUI();
}
diff --git a/Runtime/Internal/BaseChart.cs b/Runtime/Internal/BaseChart.cs
index b3536b48..098c5b07 100644
--- a/Runtime/Internal/BaseChart.cs
+++ b/Runtime/Internal/BaseChart.cs
@@ -627,6 +627,8 @@ namespace XCharts.Runtime
vh.Clear();
var maxPainter = settings.maxPainter;
var maxSeries = m_Series.Count;
+ if (painter == null || painter.index < 0 || painter.index >= maxPainter)
+ return;
var rate = Mathf.CeilToInt(maxSeries * 1.0f / maxPainter);
m_PainterUpper.Refresh();
m_PainterTop.Refresh();
diff --git a/Runtime/Internal/BaseGraph.API.cs b/Runtime/Internal/BaseGraph.API.cs
index 65aaeb13..f760da11 100644
--- a/Runtime/Internal/BaseGraph.API.cs
+++ b/Runtime/Internal/BaseGraph.API.cs
@@ -209,15 +209,20 @@ namespace XCharts.Runtime
///
/// type of image: png, jpg, exr
/// save path
- public void SaveAsImage(string imageType = "png", string savePath = "")
+ /// export resolution scale. 1 means original size
+ /// whether to recursively use lower-level UI background color
+ public void SaveAsImage(string imageType = "png", string savePath = "", float exportScale = 1f,
+ bool useRecursiveBackgroundColor = false)
{
- StartCoroutine(SaveAsImageSync(imageType, savePath));
+ StartCoroutine(SaveAsImageSync(imageType, savePath, exportScale, useRecursiveBackgroundColor));
}
- private IEnumerator SaveAsImageSync(string imageType, string path)
+ private IEnumerator SaveAsImageSync(string imageType, string path, float exportScale,
+ bool useRecursiveBackgroundColor)
{
yield return new WaitForEndOfFrame();
- ChartHelper.SaveAsImage(rectTransform, canvas, imageType, path);
+ ChartHelper.SaveAsImage(rectTransform, canvas, imageType, path, exportScale,
+ useRecursiveBackgroundColor);
}
public Vector3 GetTitlePosition(Title title)
diff --git a/Runtime/Internal/Painter.cs b/Runtime/Internal/Painter.cs
index e0c33fea..95672462 100644
--- a/Runtime/Internal/Painter.cs
+++ b/Runtime/Internal/Painter.cs
@@ -18,7 +18,11 @@ namespace XCharts.Runtime
protected bool m_Refresh;
protected Action m_OnPopulateMesh;
- public Action onPopulateMesh { set { m_OnPopulateMesh = value; } }
+ public Action onPopulateMesh
+ {
+ get { return m_OnPopulateMesh; }
+ set { m_OnPopulateMesh = value; }
+ }
public int index { get { return m_Index; } set { m_Index = value; } }
public Type type { get { return m_Type; } set { m_Type = value; } }
public void Refresh()
diff --git a/Runtime/Internal/Utilities/ChartHelper.cs b/Runtime/Internal/Utilities/ChartHelper.cs
index 9b53d2c3..abfebcc6 100644
--- a/Runtime/Internal/Utilities/ChartHelper.cs
+++ b/Runtime/Internal/Utilities/ChartHelper.cs
@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
+using XUGL;
#if dUI_TextMeshPro
using TMPro;
#endif
@@ -1060,34 +1061,359 @@ namespace XCharts.Runtime
private static extern void Download(string base64str, string fileName);
#endif
- public static Texture2D SaveAsImage(RectTransform rectTransform, Canvas canvas, string imageType = "png", string path = "")
+ private static void SetLayerRecursively(GameObject obj, int layer)
+ {
+ if (obj == null) return;
+ obj.layer = layer;
+ var trans = obj.transform;
+ for (int i = 0; i < trans.childCount; i++)
+ {
+ SetLayerRecursively(trans.GetChild(i).gameObject, layer);
+ }
+ }
+
+ private static void CloneChildrenRecursively(Transform source, Transform targetParent, int layer)
+ {
+ if (source == null || targetParent == null) return;
+ for (int i = 0; i < source.childCount; i++)
+ {
+ var child = source.GetChild(i);
+ var childClone = GameObject.Instantiate(child.gameObject, targetParent, false);
+ SetLayerRecursively(childClone, layer);
+ SyncPainterCallbacks(child, childClone.transform);
+ }
+ }
+
+ private static void SyncPainterCallbacks(Transform source, Transform clone)
+ {
+ if (source == null || clone == null) return;
+ var sourcePainter = source.GetComponent();
+ var clonePainter = clone.GetComponent();
+ if (sourcePainter != null && clonePainter != null)
+ {
+ clonePainter.onPopulateMesh = sourcePainter.onPopulateMesh;
+ clonePainter.index = sourcePainter.index;
+ clonePainter.type = sourcePainter.type;
+ clonePainter.material = sourcePainter.material;
+ clonePainter.Refresh();
+ }
+ var count = Mathf.Min(source.childCount, clone.childCount);
+ for (int i = 0; i < count; i++)
+ {
+ SyncPainterCallbacks(source.GetChild(i), clone.GetChild(i));
+ }
+ }
+
+ private static void DestroyObject(GameObject obj)
+ {
+ if (obj == null) return;
+#if UNITY_EDITOR
+ if (!Application.isPlaying)
+ GameObject.DestroyImmediate(obj, true);
+ else
+ GameObject.Destroy(obj);
+#else
+ GameObject.Destroy(obj);
+#endif
+ }
+
+ private static byte[] EncodeImage(Texture2D tex, string imageType)
{
- var cam = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
- var pos = RectTransformUtility.WorldToScreenPoint(cam, rectTransform.position);
- var width = (int)(rectTransform.rect.width * canvas.scaleFactor);
- var height = (int)(rectTransform.rect.height * canvas.scaleFactor);
- var posX = pos.x + rectTransform.rect.xMin * canvas.scaleFactor;
- var posY = pos.y + rectTransform.rect.yMin * canvas.scaleFactor;
- var rect = new Rect(posX, posY, width, height);
- var tex = new Texture2D(width, height, TextureFormat.ARGB32, false);
- tex.ReadPixels(rect, 0, 0);
- tex.Apply();
- byte[] bytes;
switch (imageType)
{
case "png":
- bytes = tex.EncodeToPNG();
- break;
+ return tex.EncodeToPNG();
case "jpg":
- bytes = tex.EncodeToJPG();
- break;
+ return tex.EncodeToJPG();
case "exr":
- bytes = tex.EncodeToEXR();
- break;
+ return tex.EncodeToEXR();
default:
Debug.LogError("SaveAsImage ERROR: not support image type:" + imageType);
return null;
}
+ }
+
+ private static float[] GetChartCornerRadius(BaseChart chart, float chartWidth, float chartHeight, float scaleFactor)
+ {
+ if (chart == null || chartWidth <= 0 || chartHeight <= 0)
+ return null;
+
+ var background = chart.GetChartComponent();
+ if (background == null || background.borderStyle == null || !background.borderStyle.roundedCorner)
+ return null;
+
+ var cornerRadius = background.borderStyle.cornerRadius;
+ if (cornerRadius == null || cornerRadius.Length == 0)
+ return null;
+
+ float brLt = 0, brRt = 0, brRb = 0, brLb = 0;
+ bool needRound = false;
+ UGL.InitCornerRadius(cornerRadius, chartWidth, chartHeight, false, false,
+ ref brLt, ref brRt, ref brRb, ref brLb, ref needRound);
+
+ if (!needRound)
+ return null;
+
+ return new float[]
+ {
+ brLt * scaleFactor,
+ brRt * scaleFactor,
+ brRb * scaleFactor,
+ brLb * scaleFactor
+ };
+ }
+
+ private static float GetRoundedRectCoverage(float x, float y, float width, float height,
+ float radiusLt, float radiusRt, float radiusRb, float radiusLb, float aaWidth = 1f)
+ {
+ if (radiusLb > 0 && x < radiusLb && y < radiusLb)
+ {
+ var dx = x - radiusLb;
+ var dy = y - radiusLb;
+ var dist = Mathf.Sqrt(dx * dx + dy * dy);
+ var delta = radiusLb - dist;
+ if (delta >= aaWidth) return 1f;
+ if (delta <= -aaWidth) return 0f;
+ return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
+ }
+ if (radiusLt > 0 && x < radiusLt && y > height - radiusLt)
+ {
+ var dx = x - radiusLt;
+ var dy = y - (height - radiusLt);
+ var dist = Mathf.Sqrt(dx * dx + dy * dy);
+ var delta = radiusLt - dist;
+ if (delta >= aaWidth) return 1f;
+ if (delta <= -aaWidth) return 0f;
+ return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
+ }
+ if (radiusRt > 0 && x > width - radiusRt && y > height - radiusRt)
+ {
+ var dx = x - (width - radiusRt);
+ var dy = y - (height - radiusRt);
+ var dist = Mathf.Sqrt(dx * dx + dy * dy);
+ var delta = radiusRt - dist;
+ if (delta >= aaWidth) return 1f;
+ if (delta <= -aaWidth) return 0f;
+ return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
+ }
+ if (radiusRb > 0 && x > width - radiusRb && y < radiusRb)
+ {
+ var dx = x - (width - radiusRb);
+ var dy = y - radiusRb;
+ var dist = Mathf.Sqrt(dx * dx + dy * dy);
+ var delta = radiusRb - dist;
+ if (delta >= aaWidth) return 1f;
+ if (delta <= -aaWidth) return 0f;
+ return Mathf.Clamp01((delta + aaWidth) / (2f * aaWidth));
+ }
+ return 1f;
+ }
+
+ private static void ApplyRoundedCornerClip(Texture2D tex, float[] cornerRadii)
+ {
+ if (tex == null || cornerRadii == null || cornerRadii.Length < 4)
+ return;
+
+ var width = tex.width;
+ var height = tex.height;
+ if (width <= 0 || height <= 0)
+ return;
+
+ var radiusLt = Mathf.Max(0, cornerRadii[0]);
+ var radiusRt = Mathf.Max(0, cornerRadii[1]);
+ var radiusRb = Mathf.Max(0, cornerRadii[2]);
+ var radiusLb = Mathf.Max(0, cornerRadii[3]);
+ if (radiusLt <= 0 && radiusRt <= 0 && radiusRb <= 0 && radiusLb <= 0)
+ return;
+
+ var colors = tex.GetPixels32();
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ var px = x + 0.5f;
+ var py = y + 0.5f;
+ var coverage = GetRoundedRectCoverage(px, py, width, height,
+ radiusLt, radiusRt, radiusRb, radiusLb, 1f);
+ if (coverage <= 0f)
+ {
+ var index = y * width + x;
+ var color = colors[index];
+ color.a = 0;
+ colors[index] = color;
+ }
+ else if (coverage < 1f)
+ {
+ var index = y * width + x;
+ var color = colors[index];
+ color.a = (byte)Mathf.Clamp(Mathf.RoundToInt(color.a * coverage), 0, 255);
+ colors[index] = color;
+ }
+ }
+ }
+ tex.SetPixels32(colors);
+ tex.Apply();
+ }
+
+ private static Color32 GetBackgroundColorRecursive(Transform parent)
+ {
+ if (parent == null) return new Color32(255, 255, 255, 255);
+
+ // Try to find Image components with colors in child nodes
+ for (int i = 0; i < parent.childCount; i++)
+ {
+ var child = parent.GetChild(i);
+ var image = child.GetComponent();
+ if (image != null && image.enabled && child.gameObject.activeInHierarchy)
+ {
+ var color = image.color;
+ if (color.a > 0)
+ {
+ // Found a visible background image
+ color.a = 1f; // Make it fully opaque for proper blending
+ return color;
+ }
+ }
+ // Recursively search child nodes
+ var foundColor = GetBackgroundColorRecursive(child);
+ if (foundColor.a > 0)
+ return foundColor;
+ }
+
+ return Color.white;
+ }
+
+ public static Texture2D SaveAsImage(RectTransform rectTransform, Canvas canvas, string imageType = "png",
+ string path = "", float exportScale = 1f, bool useRecursiveBackgroundColor = false)
+ {
+ if (rectTransform == null || canvas == null)
+ return null;
+
+ var clampedExportScale = Mathf.Max(1f, exportScale);
+ var scaleFactor = canvas.scaleFactor <= 0 ? 1f : canvas.scaleFactor;
+ var outputScaleFactor = scaleFactor * clampedExportScale;
+ var width = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.width * outputScaleFactor));
+ var height = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.height * outputScaleFactor));
+ var chart = rectTransform.GetComponent();
+ var cornerRadii = GetChartCornerRadius(chart, rectTransform.rect.width, rectTransform.rect.height, outputScaleFactor);
+
+ Texture2D tex = null;
+ var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
+ var antiAliasing = QualitySettings.antiAliasing > 0 ? QualitySettings.antiAliasing : 4;
+ rt.antiAliasing = Mathf.Clamp(antiAliasing, 1, 8);
+
+ var oldActive = RenderTexture.active;
+ var captureLayer = 31;
+ var rootObj = new GameObject("xcharts_save_image_root");
+ var camObj = new GameObject("xcharts_save_image_camera");
+ var canvasObj = new GameObject("xcharts_save_image_canvas");
+ var contentObj = new GameObject("xcharts_save_image_content", typeof(RectTransform));
+
+ try
+ {
+ SetLayerRecursively(rootObj, captureLayer);
+ SetLayerRecursively(camObj, captureLayer);
+ SetLayerRecursively(canvasObj, captureLayer);
+ SetLayerRecursively(contentObj, captureLayer);
+
+ camObj.transform.SetParent(rootObj.transform, false);
+ var camera = camObj.AddComponent();
+ camera.clearFlags = CameraClearFlags.SolidColor;
+
+ // Get background color - try multiple sources for better results
+ Color32 bgColor = new Color32(255, 255, 255, 255);
+ var chartParent = rectTransform.parent;
+
+ // First, try to get from chart's Background component
+ if (chart != null)
+ {
+ bgColor = chart.GetChartBackgroundColor();
+ //bgColor.a = 255;
+ }
+
+ // If enabled, find background color recursively from sibling nodes
+ if (useRecursiveBackgroundColor && (bgColor.a < 255 ||
+ (bgColor.r == 255 && bgColor.g == 255 && bgColor.b == 255)))
+ {
+ var siblingBgColor = GetBackgroundColorRecursive(chartParent);
+ if (siblingBgColor.a > 0)
+ bgColor = siblingBgColor;
+ }
+
+ camera.backgroundColor = bgColor;
+
+ camera.cullingMask = 1 << captureLayer;
+ camera.orthographic = true;
+ camera.orthographicSize = height / 2f;
+ camera.nearClipPlane = -100;
+ camera.farClipPlane = 100;
+ camera.allowHDR = false;
+ camera.allowMSAA = rt.antiAliasing > 1;
+ camera.targetTexture = rt;
+
+ canvasObj.transform.SetParent(rootObj.transform, false);
+ var captureCanvas = canvasObj.AddComponent