【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出

news2024/11/20 13:26:57

 书接上回:【Unity编辑器扩展】(一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用_TopGames的博客-CSDN博客

 工具使用预览:

 工具目标:

1. 实现将psd解析生成为UI预制体,并导出UI图片。需支持UGUI和TextMeshProGUI, 如Button、Toggle、ScrollView、Text、Slider、Dropdown、Image、RawImage以及纯色填充图等UGUI元素(ps:纯色填充图:例如PS的纯色图层,通过调整Unity的Color可以达到相同效果,无需导出为纯色图片,以节省资源大小)

2. 要求工具使用简单便捷、并且能满足不同需求。支持自动解析类型和手动调节UI类型和层级。

3. 并且工具不能高度依赖或要求UI设计师必须遵守某种规范。

4. 工具只需要psd文件。不能依赖PS或Unity以外的第三方软件(目前已有的psd转ugui工具中通常依赖安装PS脚本,且对PS版本有严格要求,需强制UI设计师安装使用脚本,这很不方便且难以维护)

工具和工作流设计:

psd图层结构如图所示:

工具实现主要需解决以下问题: 

1. 节点层级

从psd图层结构可以看出,psd图层与Unity Transform树状节点完全不同,psd只有"LayerGroup(组)"的概念,一个图层不能成为另一个图层的子图层,只能成为组的子图层。而UGUI对节点的层级有严格的要求,比如Button节点下通常会有个Text显示按钮文字,Text需作为Button的子节点进行布局。而psd中Button图片图层和文字图层无法以父子节点的形式存在。

所以,工具需要把psd图层解析为Unity 节点,每个节点绑定一个psd图层或组,这样程序就可以任意调整节点层级以满足自己的需求。

psd解析为Unity节点以支持层级调整

2. UI类型

psd图层类型最终只能归为两种,图片或文本。然而图片可以是Button、可以是Image,也可以是ScrollView的背景图,所以就需求为psd图层做类型标记,以便自动解析为对应的UI类型。图层名字是存放这一信息的最佳选择,但由于种种原因,你不能强制要求UI设计师遵守某种规范。

所以工具不能过于依赖UI设计师规范化,解析psd图层时先按照命名规范初始化UI类型,同时支持手动以下拉菜单的形式选择UI类型。

psd图层名称以.button(.btn)结尾命名规范,从而实现UI类型标记,如果没有标记则图像图层默认为常用的Image类型(可配RawImage),文本图层默认为Text类型(可配TextMeshProUGUI)。UI类型标识不区分大小写、并且支持自定义配置多种匹配字符,如:Button对应的标识可以配置为bt、btn、button等。

UI类型解析配置界面:

UI类型解析支持自由配置

通过图层命名规范标记UI类型

 3. 复合型UI类型

 例如ScrollView,有多个UI节点组成,一个ScrollView有背景图、Viewport遮罩图、水平滚动条/垂直滚动条(包含背景和滑块两张图)。这种情况就不是标记一个psd图层能够解决的了。因此可以当成一个组处理,比如把组当成ScrollView,ScrollView的背景图、遮罩图、滚动条对应的图层都包含在这个组中。

4. 实时预览

psd转ugui编辑界面需支持预览效果,如支持预览每个图层或组的图像、UI类型、是否导出图片等设置。

工具面板组成和功能

工作流总结:

1. 将psd文件导入Unity, 右键psd文件,选择Psd2UIForm Editor菜单,自动把psd解析为prefab节点树状图,并打开prefab

2. 若UI设计师未按照规范制作,可手动根据需求调节UI类型或UI层级(可选)

3. 点击扩展按钮,重新解析psd、导出碎图或生成UI界面prefab。(工具会自动记录psd文件最后修改时间,若发生变更,自动弹窗提示是否重新解析)

 功能实现:

一,图层解析(psd转节点树状图prefab)

由于PS图层没有树状节点的概念,只有LayerGroup(组)类似树状节点。并且图层遮挡顺序自下而上,与Unity节点相反。Aspose.PSD解析出的layers数组是根据psd图层栏自下而上遍历所有图层。并且如果下一图层属于其它组内图层,还会出现一个SelectionLayer层,然后继续遍历图层。而这个SelectionLayer作用类似开始标签,如<SectionDividerLayer>和<LayerGroup>是组的开始/结束标签。

Aspose.PSD解析图层数组layers的顺序和索引如下:

1. 定义一个PsdLayerNode MonoBehavior脚本,用于关联到每个psd图层节点,每个PsdLayerNode绑定着一个PS图层(Layer)

2. 把psd文件解析为PsdLayerNode节点树状图:

/// <summary>
        /// 把Psd图层解析成节点prefab
        /// </summary>
        /// <param name="psdPath"></param>
        /// <returns></returns>
        public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null)
        {
            if (!File.Exists(psdFile))
            {
                Debug.LogError($"Error: Psd文件不存在:{psdFile}");
                return false;
            }
            var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;
            if (texImporter.textureType != TextureImporterType.Sprite)
            {
                texImporter.textureType = TextureImporterType.Sprite;
                texImporter.mipmapEnabled = false;
                texImporter.alphaIsTransparency = true;
                texImporter.SaveAndReimport();
            }

            var prefabFile = GetPsdLayerPrefabPath(psdFile);
            var rootName = Path.GetFileNameWithoutExtension(prefabFile);

            bool needDestroyInstance = instanceRoot == null;
            Psd2UIFormConverter rootLayer;
            if (instanceRoot != null)
            {
                rootLayer = instanceRoot;
                //清空已有节点重新解析
                for (int i = rootLayer.transform.childCount - 1; i >= 0; i--)
                {
                    GameObject.DestroyImmediate(rootLayer.transform.GetChild(i).gameObject);
                }
            }
            else
            {
                rootLayer = CreatePsdLayerRoot(rootName);
            }

            rootLayer.SetPsdAsset(AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(psdFile));
            rootLayer.psdAssetChangeTime = GetAssetChangeTag(psdFile);

            var psdOpts = new PsdLoadOptions()
            {
                LoadEffectsResource = true
            };
            using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage)
            {
                List<GameObject> layerNodes = new List<GameObject> { rootLayer.gameObject };

                for (int i = 0; i < psd.Layers.Length; i++)
                {
                    var layer = psd.Layers[i];
                    var curLayerType = layer.GetLayerType();

                    if (curLayerType == PsdLayerType.SectionDividerLayer)
                    {
                        var layerGroup = (layer as SectionDividerLayer).GetRelatedLayerGroup();
                        var layerGroupIdx = ArrayUtility.IndexOf(psd.Layers, layerGroup);
                        var layerGropNode = CreatePsdLayerNode(layerGroup, layerGroupIdx);
                        layerNodes.Add(layerGropNode.gameObject);
                    }
                    else if (curLayerType == PsdLayerType.LayerGroup)
                    {
                        var lastLayerNode = layerNodes.Last();
                        layerNodes.Remove(lastLayerNode);

                        if (layerNodes.Count > 0)
                        {
                            var parentLayerNode = layerNodes.Last();
                            lastLayerNode.transform.SetParent(parentLayerNode.transform);
                        }
                    }
                    else
                    {
                        var newLayerNode = CreatePsdLayerNode(layer, i);
                        newLayerNode.transform.SetParent(layerNodes.Last().transform);
                        newLayerNode.transform.localPosition = Vector3.zero;
                    }
                }

            }
            PrefabUtility.SaveAsPrefabAsset(rootLayer.gameObject, prefabFile, out bool savePrefabSuccess);
            if (needDestroyInstance) GameObject.DestroyImmediate(rootLayer.gameObject);
            AssetDatabase.Refresh();
            if (savePrefabSuccess && AssetDatabase.GUIDFromAssetPath(StageUtility.GetCurrentStage().assetPath) != AssetDatabase.GUIDFromAssetPath(prefabFile))
            {
                PrefabStageUtility.OpenPrefab(prefabFile);
            }
            return savePrefabSuccess;
        }

        private void SetPsdAsset(Sprite texture2D)
        {
            this.psdAsset = texture2D;
            if (string.IsNullOrWhiteSpace(this.uiFormName))
            {
                this.uiFormName = this.psdAsset.name;
            }
        }

        /// <summary>
        /// 获取解析好的psd layers文件
        /// </summary>
        /// <param name="psd"></param>
        /// <returns></returns>
        public static string GetPsdLayerPrefabPath(string psd)
        {
            return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");
        }
        private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName)
        {
            var node = new GameObject(rootName);
            node.gameObject.tag = "EditorOnly";
            var layerRoot = node.AddComponent<Psd2UIFormConverter>();
            return layerRoot;
        }
        private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx)
        {
            string nodeName = layer.Name;
            if (string.IsNullOrWhiteSpace(nodeName))
            {
                nodeName = $"PsdLayer-{bindLayerIdx}";
            }
            else
            {
                if (Path.HasExtension(layer.Name))
                {
                    nodeName = Path.GetFileNameWithoutExtension(layer.Name);
                }
            }
            var node = new GameObject(nodeName);
            node.gameObject.tag = "EditorOnly";
            var layerNode = node.AddComponent<PsdLayerNode>();
            layerNode.BindPsdLayerIndex = bindLayerIdx;
            InitLayerNodeData(layerNode, layer);
            return layerNode;
        }

二、显示预览图

事实上步骤一的解析只是把psd图层转换为可修改层级的GameObject节点,此时处于编辑阶段, 节点上也没有任何Render组件,并没有(也不应该)把图层导出成图片给节点显示。

编辑阶段还不明确用户需要导出图片的图层(而且若导出图片会触发Unity自动导入资源,影响体验),出于工具运行效率和用户体验的考量,不能直接生成图片文件。而是设计为用户首次点选节点时,把节点对应的图层转换为Texture2D实例以便预览,用户没点击的节点就不会转换。当点击导出碎图按钮时,标记为导出但没有转换为Texture2D实例的图层再执行转换操作。这样就大大提高了工具解析速度和效率。

1. 点选节点时,把psd图层的Bitmap转成Unity的Texture2D类型作为预览图(支持把psd整个组转换成一张图):

/// <summary>
        /// 把psd图层转成Texture2D
        /// </summary>
        /// <param name="psdLayer"></param>
        /// <returns>Texture2D</returns>
        public Texture2D ConvertPsdLayer2Texture2D()
        {
            if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;
            var parentNode = transform.parent.GetComponent<PsdLayerNode>();
            Rectangle bounds;
            if (parentNode == null || (this.LayerType == PsdLayerType.LayerGroup && parentNode.LayerType != PsdLayerType.LayerGroup))
            {
                bounds = BindPsdLayer.GetFixedLayerBounds();
            }
            else
            {
                bounds = BindPsdLayer.Bounds;
            }
            MemoryStream ms = new MemoryStream();
            var pngOpt = new PngOptions()
            {
                ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha
            };
            BindPsdLayer.Save(ms, pngOpt, bounds);

            var buffer = new byte[ms.Length];
            ms.Position = 0;
            ms.Read(buffer, 0, buffer.Length);

            Texture2D texture = new Texture2D(bounds.Width, bounds.Height);
            texture.alphaIsTransparency = true;
            texture.LoadImage(buffer);
            texture.Apply();
            ms.Dispose();
            return texture;
        }

这里把psd组转成一张图会有一个坑,当组为最外层组时,Aspose.PSD库无法获取正确的图片区域(Rectangle),导致生成的图片有位置偏移。为了绕过这个坑我是通过遍历计算组的所有子图层区域,从而得到一个合并区域:

/// <summary>
        /// 修复当LayerGroup为第一层时,对应Bounds错位
        /// </summary>
        /// <param name="layerGroup"></param>
        /// <returns></returns>
        public static Rectangle GetFixedLayerBounds(this Layer layerGroup)
        {
            if (layerGroup.GetLayerType() != PsdLayerType.LayerGroup)
            {
                return layerGroup.Bounds;
            }
            //根据子图层算出包围所有子图层的最小包围盒
            var subLayers = (layerGroup as LayerGroup).Layers;
            int minLeft = int.MaxValue;
            int minTop = int.MaxValue;
            int maxRight = int.MinValue;
            int maxBottom = int.MinValue;
            foreach (var item in subLayers)
            {
                var itemTp = item.GetLayerType();
                if (itemTp == PsdLayerType.Unknown || itemTp == PsdLayerType.LayerGroup || itemTp == PsdLayerType.SectionDividerLayer) continue;
                var itemBounds = item.Bounds;
                if (item.Left < minLeft) minLeft = item.Left;
                if (item.Top < minTop) minTop = item.Top;
                if (item.Right > maxRight) maxRight = item.Right;
                if (item.Bottom > maxBottom) maxBottom = item.Bottom;
            }
            //var result = new Rectangle(new Point(minLeft, minTop), new Size(maxRight - minLeft, maxBottom - minTop));
            var result = new Rectangle()
            {
                Top = minTop,
                Left = minLeft,
                Right = maxRight,
                Bottom = maxBottom
            };
            return result;
        }

2. 重写HasPreviewGUI、OnPreviewGUI、GetInfoString函数以自定义显示Inspector面板的预览界面:

[CanEditMultipleObjects]
    [CustomEditor(typeof(PsdLayerNode))]
    public class PsdLayerNodeInspector : Editor
    {
        PsdLayerNode targetLogic;
        private void OnEnable()
        {
            targetLogic = target as PsdLayerNode;
            targetLogic.RefreshLayerTexture();
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            base.OnInspectorGUI();
            EditorGUI.BeginChangeCheck();
            {
                targetLogic.UIType = (GUIType)EditorGUILayout.EnumPopup("UI Type", targetLogic.UIType);
                if (EditorGUI.EndChangeCheck())
                {
                    targetLogic.SetUIType(targetLogic.UIType);
                }
            }
            serializedObject.ApplyModifiedProperties();
        }

        public override bool HasPreviewGUI()
        {
            return targetLogic.PreviewTexture != null;
        }

        public override void OnPreviewGUI(Rect r, GUIStyle background)
        {
            GUI.DrawTexture(r, targetLogic.PreviewTexture, ScaleMode.ScaleToFit);
            //base.OnPreviewGUI(r, background);
        }
        public override string GetInfoString()
        {
            //var size = targetLogic.PreviewTexture.Size();
            return targetLogic.LayerInfo;
        }
    }
点击节点预览图层

3. Scene界面显示图层边界debug框

PS的坐标系左上角为原点(0,0),而Unity Scene界面坐标系原点为视口中心。把Aspose.PSD Rectangle转换为Unity Rect:

/// <summary>
        /// 获取psd图层的Rect边框(Unity坐标系)
        /// </summary>
        /// <param name="layer"></param>
        /// <returns></returns>
        public static Rect GetLayerRect(this Layer layer)
        {
            var layerTp = layer.GetLayerType();
            int left, right, top, bottom;
            if (layerTp == PsdLayerType.LayerGroup)
            {
                var bounds = layer.GetFixedLayerBounds();
                left = bounds.Left;
                right = bounds.Right;
                top = bounds.Top;
                bottom = bounds.Bottom;
            }
            else
            {
                left = layer.Left;
                right = layer.Right;
                top = layer.Top;
                bottom = layer.Bottom;
            }

            float halfWidth = Mathf.Abs(right - left) * 0.5f;
            float halfHeight = Mathf.Abs(bottom - top) * 0.5f;
            var canvasSize = layer.Container.Size;
            Rect result = new Rect(left + halfWidth - canvasSize.Width * 0.5f, canvasSize.Height - (top + halfHeight) - canvasSize.Height * 0.5f, right - left, bottom - top);
            return result;
        }

 为PsdLayerNode写一个CustomEditor,当点选节点时就会触发OnEnable方法,在首次OnEnable时生成预览Texture2D:

[CanEditMultipleObjects]
    [CustomEditor(typeof(PsdLayerNode))]
    public class PsdLayerNodeInspector : Editor
    {
        PsdLayerNode targetLogic;
        private void OnEnable()
        {
            targetLogic = target as PsdLayerNode;
            targetLogic.RefreshLayerTexture();
        }
    }


[ExecuteInEditMode]
    [DisallowMultipleComponent]
    public class PsdLayerNode : MonoBehaviour
    {
        [ReadOnlyField] public int BindPsdLayerIndex = -1;
        [ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;
        [SerializeField] public bool ExportImage;
        [HideInInspector] public GUIType UIType;
        public Texture2D PreviewTexture { get; private set; }
        public string LayerInfo { get; private set; }
        public Rect LayerRect { get; private set; }
        public PsdLayerType LayerType { get => mLayerType; }
        /// <summary>
        /// 绑定的psd图层
        /// </summary>
        private Layer mBindPsdLayer;

        public Layer BindPsdLayer
        {
            get => mBindPsdLayer;
            set
            {
                mBindPsdLayer = value;
                mLayerType = mBindPsdLayer.GetLayerType();
                LayerRect = mBindPsdLayer.GetLayerRect();
                LayerInfo = $"{LayerRect}";
            }
        }
        
        public bool RefreshLayerTexture(bool forceRefresh = false)
        {
            if (!forceRefresh && PreviewTexture != null)
            {
                return true;
            }

            if (BindPsdLayer == null || BindPsdLayer.Disposed) return false;

            var pngOpt = new PngOptions
            {
                ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha
            };
            if (BindPsdLayer.CanSave(pngOpt))
            {
                if (PreviewTexture != null)
                {
                    DestroyImmediate(PreviewTexture);
                }
                PreviewTexture = this.ConvertPsdLayer2Texture2D();
            }
            return PreviewTexture != null;
        }
    }

 在根节点的Psd2UIFormConverter脚本的OnGizmos方法中统一绘制所有图层节点边框:

private void OnDrawGizmos()
        {
            if (drawLayerRectGizmos)
            {
                var nodes = this.GetComponentsInChildren<PsdLayerNode>();
                Gizmos.color = drawLayerRectGizmosColor;
                foreach (var item in nodes)
                {
                    if (item.NeedExportImage())
                    {
                        Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);
                    }

                }
            }
        }
图层区域debug框

三,导出UI碎图 

 可通过勾选控制需要导出的图片,被其它UI元素引用的图层即使不勾选也会导出图片。导出的图片Unity默认为Texture类型,所以还需要用代码转换为Sprite类型。

 代码实现:

/// <summary>
        /// 导出psd图层为Sprites碎图
        /// </summary>
        /// <param name="psdAssetName"></param>
        internal void ExportSprites()
        {
            var exportLayers = this.GetComponentsInChildren<PsdLayerNode>().Where(node => node.NeedExportImage());
            var exportDir = UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(PsdAssetName), this.uiFormName);
            if (!Directory.Exists(exportDir))
            {
                Directory.CreateDirectory(exportDir);
            }
            int exportIdx = 1;
            int totalCount = exportLayers.Count();
            foreach (var layer in exportLayers)
            {
                if (layer.RefreshLayerTexture())
                {
                    var bytes = layer.PreviewTexture.EncodeToPNG();
                    var imgName = string.IsNullOrWhiteSpace(layer.name) ? $"UI_Layer_{layer.BindPsdLayerIndex}" : layer.name;
                    var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");
                    if (File.Exists(imgFileName))
                    {
                        imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + $"_{layer.BindPsdLayerIndex}.png");
                    }
                    EditorUtility.DisplayProgressBar($"导出({exportIdx}/{totalCount})", $"导出Image文件:{imgFileName}", exportIdx / (float)totalCount);
                    File.WriteAllBytes(imgFileName, bytes);
                }
            }
            EditorUtility.ClearProgressBar();
            AssetDatabase.Refresh();
            ConvertTextureToUIByDir(exportDir);
        }

/// <summary>
        /// 把指定目录下所有Texture转换为Sprite类型
        /// </summary>
        /// <param name="dir"></param>
        public static void ConvertTextureToUIByDir(string dir)
        {
            var guidArr = AssetDatabase.FindAssets("t:Texture", new string[] { dir });
            foreach (var item in guidArr)
            {
                var assetName = AssetDatabase.GUIDToAssetPath(item);
                var texImporter = AssetImporter.GetAtPath(assetName) as TextureImporter;
                if (texImporter == null) continue;
                texImporter.textureType = TextureImporterType.Sprite;
                texImporter.spriteImportMode = SpriteImportMode.Single;
                texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
                texImporter.alphaIsTransparency = true;
                texImporter.mipmapEnabled = false;
                texImporter.SaveAndReimport();
            }
        }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/568714.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文心一言 VS 讯飞星火 VS chatgpt (22)-- 算法导论4.2 2题

二、为Strassen算法编写伪代码。 文心一言&#xff1a; Strassen算法是一种用于矩阵乘法的算法&#xff0c;它的核心思想是通过不断将原始矩阵分割成较小的子矩阵来实现高效的乘法运算。以下是Strassen算法的伪代码&#xff1a; function Strassen(A, B): if A[0][0] 0 or…

优化|数学软件是如何求解线性方程Ax=b ?

编者按 对于大家来说&#xff0c;我们从学会多项式开始就得和求解矩阵方程打交道。大学之前靠手算&#xff0c;到了大学阶段我们学会了使用科学计算软件&#xff0c;然后只需要输入简单的一行指令 x A \ b x A \backslash b xA\b&#xff0c;就可以轻轻松松求解方程组 A x …

html实现酷炫科技风大屏风格模板(附源码)

文章目录 1.设计来源1.1 大屏主界面1.2 弹框界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/130870963 html实现酷炫科技风大屏风格模板源码 &#xff0c;html大屏源码…

docker容器postgres数据导出命令及还原数据命令

参考资料 docker控制postgers容器导入导出数据_docker 导出数据库_奔跑的痕迹的博客-CSDN博客 --进入容器 docker exec -it 容器名称或容器id /bin/bash 以下命令是在进入容器执行的 --导出单张表的备份语句&#xff08;copy模式&#xff09; pg_dump -h 127.0.0.1 -U …

授权管理再工控安全中起到什么作用?

随着计算机技术、通信技术和控制技术的发展&#xff0c;工业自动化控制已经开始向网络化方向发展&#xff0c;从最初的CCS&#xff08;计算机集中控制系统&#xff09;&#xff0c;到第二代的DCS&#xff08;分散控制系统&#xff09;&#xff0c;发展到现在流行的FCS&#xff…

力扣 1775.通过最少操作次数使数的和相等、1014.最佳观光组合、33.搜索旋转排序数组

算法总结 最近作者在坚持每日一道中等难度算法题&#xff0c;也是作者考核时经常会碰到的难度&#xff0c;由于经常是到22:30才意识到自己并没有写算法&#xff0c;因此都是打开LeetCode网站随机一题&#xff0c;并未系统性的去学习&#xff0c;这一点值得反思。在做题过程中经…

航天科技AIRIOT物联会【智慧物联主题沙龙】在沈阳举办

2023年5月24日&#xff0c;由航天科技控股集团股份有限公司&#xff08;简称“航天科技”&#xff09;智慧物联事业部主办的《AIRIOT物联会-智慧物联主题分享沙龙》在沈阳举办&#xff0c;此次会议邀请到来自光伏、燃气、能源、水务、园区、工厂等行业的众多企业代表参加&#…

0起步用GPT+知乎赚了点小钱,人人可复制

大家好&#xff0c;我是五竹。 前段时间分享了一篇关于用ChatGPT赚点小钱的实战&#xff1a;TMD&#xff0c;被人偷窥了一个月&#xff01;结果上周末的时候在知乎追了一个关于Claude的热点&#xff0c;发布了一篇注册Claude的文章&#xff0c;结果小小的“爆了”一下&#xf…

Qt文件系统源码分析—第五篇QTemporaryFile

深度 本文主要分析Windows平台&#xff0c;Mac、Linux暂不涉及 本文只分析到Win32 API/Windows Com组件/STL库函数层次&#xff0c;再下层代码不做探究 本文QT版本5.15.2 类关系图 QTemporaryFile继承QFile QFile、QSaveFile继承QFileDevice QFileDevice继承QIODevice Q…

如何查看一个 docker 镜像有哪些版本

如何查看一个 docker 镜像有哪些版本 因为通过 docker search 并不能查看某个镜像的版本信息&#xff0c;如我需要特定版本的 redis 那怎么办呢~ 本文提供了如下几种方式&#xff0c;大家可以分别逐个尝试下~ 为什么有几种方式呢&#xff0c;因为官方的查找镜像网址 Docker H…

使用audition测试USBaudio数据回传延时

一&#xff0c;简介 本文主要介绍如何使用Audition软件来测试STM32 USB audio上行音频数据的延时。 二&#xff0c;准备工作 Audition&#xff0c;ASIO驱动&#xff0c;STM32枚举的USB Audio高速声卡测试板。 二&#xff0c;硬件连接 将STM32的IIS的data in和data out使用…

四款AI视频翻译产品横评

本文内容节选自 Paxi.ai 的文章分享&#xff0c;从其中摘录了我觉得有意思的一部分。Paxi.ai 是一个基于 GPT-4 打造的帮用户快速使用AI的AI工具&#xff0c;通过与它的小助手对话可以了解各种AI的产品功能和使用方式。对本文内容感兴趣的朋友可以上他们官网查看。 有没有想过把…

go embed 实现gin + vue静态资源嵌入

前言 golang1.16出来以后&#xff0c;早就有打算把ckman项目的前端代码打包更换成embed。在此之前&#xff0c;使用的是pkger进行的打包。 但是今天打包时却报了个错&#xff1a; 而且通过各种手段尝试均无果之后&#xff0c;果断把决定立即将其更换为embed进行资源嵌入管理。…

华为OD机试真题 Java 实现【寻找符合要求的最长子串】【2023Q1 200分】

一、题目描述 给定一个字符串 s &#xff0c;找出这样一个子串&#xff1a; 该子串中的任意一个字符最多出现2次&#xff1b;该子串不包含指定某个字符&#xff1b; 请你找出满足该条件的最长子串的长度。 二、输入描述 第一行为要求不包含的指定字符&#xff0c;为单个字…

一个神奇的工具,让URL地址都变成了“ooooooooo“

一个神奇的工具&#xff0c;让URL地址都变成了"ooooooooo" 一、核心代码二、URL编码/解码 最近发现一个有意思工具&#xff0c;就是将一个URL地址转换为都是 ooooooooo 的样子&#xff0c;通过转换后的地址访问可以转换回到原始地址&#xff0c;转换的逻辑有点像短链…

mysql8绿色版安装教程

最近在安装mysql8绿色版&#xff0c;以下是安装过程中的一些步骤&#xff1a; 1&#xff1a;解压zip压缩包至想要安装的目录&#xff0c;比如解压到D:\mysql\mysql-8.0.29-winx64 2&#xff1a;在解压目录D:\mysql-8.0.29-winx64中创建MySQL配置文件my.ini 配置文件my.ini内容…

Unity3D安装:离线安装 Unity

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 在没有 Hub 的情况下离线安装 Unity Unity 下载助手 (Download Assistant) 支持离线部署。在这种部署方式中&#xff0c;可下载用于安装 Unity 的所有文件&#xff0c;然后生成脚本…

【日常】如何增加粉丝的粘性?

【日常】如何增加粉丝的粘性&#xff1f; 1、前言2、官方活动3、作品质量4、打造自己的社区 1、前言 不知不觉间&#xff0c;铁粉已经过千了&#xff0c;粉丝数也即将两万&#xff0c;总而言之&#xff0c;越努力&#xff0c;越幸运&#xff0c;付出就有回报&#xff08;当然在…

面了个阿里拿36K出来的,真是砂纸擦屁股,给我漏了一手

今年的春招已经结束&#xff0c;很多小伙伴收获不错&#xff0c;拿到了心仪的 offer。 各大论坛和社区里也看见不少小伙伴慷慨地分享了常见的面试题和八股文&#xff0c;为此咱这里也统一做一次大整理和大归类&#xff0c;这也算是划重点了。 俗话说得好&#xff0c;他山之石…

减肥瘦身自律APP软件开发功能有哪些?

减肥瘦身是很多女人一生都在奋斗的目标&#xff0c;如果找不对方法&#xff0c;减肥效果事倍功半还可能会反弹&#xff0c;所以越来越多的人推崇健康科学的减肥理念&#xff0c;把瘦身的重心转移到饮食和运动管理上&#xff0c;于是市场上出现了减肥瘦身自律类的APP软件&#x…