做为学习WF的学习成果,勇哥写了这个Mtv点歌软件。
在每学一门语言或者框架时,勇哥都写过类似功能的软件。
浪费点文章的位置把其它版本的放这里给各位把玩一下吧:
------------------------------
Python版本的:
《Python Tkinter 学习成果:点歌软件music》
plc+单片机+c#版本:
《欧姆龙cp1h常用指令学习(十四)练习篇一:红外线遥控点歌》
C#版本的:
vc++与Qt版:
《vs2013+Qt的学习小程序:点歌软件》
用FW来实现这种软件的功能,从功能上来讲是属于把简单的问题复杂化,但是做为FW的入门练习勇哥觉得还是不错的。
(一) 界面与说明
界面如下图所示。

说明一下:
(1) 宿主程序的功能需求
功能很简单:
全部歌曲页的内容是读取了指定目录下的全部扩展名为*.mkv, *.mp3的音视频文件。
选中的歌曲页是由“全部歌曲”页面添加过来的歌曲。 可以删除该页选中的歌曲。
两个页都可以搜索歌曲,搜索框用户通过输入首拼方式查询结果,并更新列表。(首拼示例: 一壶老酒 YHLJ)
双击歌曲名字,调用视频播放器播放。
(2) 界面上流程编辑功能分三个部分: 工具箱、工具属性面板、流程编辑区
(3) 按宿主程序功能的需求,勇哥把流程分成六个,如下:

(4) 宿主程序中执行6个流程的时机如下:
更新歌曲: 点击![]()
显示选中的歌曲: 点击![]()
删除歌曲: 点击![]()
添加到选中歌曲: 点击 ![]()
搜索歌曲:
文本框内容改变时
播放歌曲: 双击歌曲列表的项目时
(5) 流程截图如下:

(更新歌曲)

(显示选中的歌曲)

(删除歌曲)

(添加到选中歌曲)

(搜索歌曲)

(播放歌曲)
(二)程序结构
程序结构中:
Mtv.ToolKit.Help, mtvMainform都是OSGI的插件模块。它们都是类库。
有关OSGI的模块知识,详见勇哥另一篇文章《OSGI.NET 模块化示例演示 》
工程mtv是应用程序,它会载入插件mtvMainform显示窗体。

(三) 知识点备注
(1)获取并启动主界面
OSGI的主控程序主要做两件事:
启动模块运行时
获取并启动主界面
有关主控程序的知识,详见勇哥另一个贴子《创建OSGI主程序》
下面是主控程序的代码,在programe.cs中。
private static void RunApplication()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (BundleRuntime bundleRuntime = new BundleRuntime())
{
bundleRuntime.Start();
// 获取Form服务,并运行。
Form mainForm = bundleRuntime.GetFirstOrDefaultService<Form>();
if (mainForm != null)
{
Application.Run(mainForm);
}
}
}工程mtvMainform做为OSGI的一个模块启动。
它由启动器Activator.cs启动。
启动器中下面这句话注册全局服务,注册了我们要启动的窗体FormMtv。
context.AddService<Form>(MainForm);
启动器完整代码如下:
using Mtv.ToolKit.Help;
using Mtv.ToolKit.Help.model;
using System;
using System.Activities.Presentation.Toolbox;
using System.Activities.Statements;
using System.Collections.Generic;
using System.ServiceModel.Activities;
using System.Text;
using System.Windows.Forms;
using UIShell.OSGi;
namespace mtvMainform
{
internal delegate void CloseFormDelegate();
public class Activator : IBundleActivator
{
private FormMtv MainForm { get; set; }
public static IBundleContext Context { get; set; }
public void Start(IBundleContext context)
{
Context = context;
MainForm = new FormMtv();
context.AddService<Form>(MainForm);
}
public void Stop(IBundleContext context)
{
context.RemoveService<Form>(MainForm);
CloseFormDelegate closeFormDel = delegate()
{
MainForm.Close();
};
if (!MainForm.IsDisposed)
{
MainForm.BeginInvoke(closeFormDel);
}
}
}
}(2)流程设计器、流程工具箱、流程属性是怎么安置到winform界面上的。
虽然workflow是基于wpf的,但是可以通过ElementHost控件(wpf交互功能)安置在winform上面。
主界面上放置了三个ElementHost控件。

下面的代码为三个elementHost控件赋值。
private factory factory = new factory();
public string Title { get; set; }
public FormMtv()
{
InitializeComponent();
factory.RegisterMetadata();
elementHost1.Child = factory.LoadToolBox(0); //工具箱
elementHost3.Child = factory.LoadDesigner(""); //流程设计器
elementHost2.Child = factory.LoadPropertyInspector(); //属性
}(3)运行工作流程
如文章开头所述,程序定义了6个工作流程,与界面的控件操作进行绑定。
下面是运行工作流程的代码:
参数fname传入流程的名字。
参数designer是流程的设计器,如果指定,则会启动流程追踪功能(下面会谈到什么是流程追踪)。
参数 param是传入流程中的某些活动所需要的输出与输出参数。
private WFDesignDebugTracking tracker = null;
/// <summary>
/// 运行工作流
/// </summary>
/// <param name="fname">传入流程名字</param>
/// <param name="designer">designer!=null 追踪</param>
/// <param name="param">有参数则传参数</param>
/// <returns></returns>
public bool RunFlow(string fname, WorkflowDesigner designer=null, IDictionary<string, object> param = null)
{
try
{
string filePath = AppDomain.CurrentDomain.BaseDirectory + "flowXmal\\" + fname;
DynamicActivity activityXaml = ActivityXamlServices.Load(filePath) as DynamicActivity;
WorkflowApplication instance = null;
if (null == param)
instance = new WorkflowApplication(activityXaml);
else
instance = new WorkflowApplication(activityXaml, param);
tracker = null;
if (designer != null)
{
tracker = new WFDesignDebugTracking(designer);
instance.Extensions.Add(tracker);
}
instance.Extensions.Add(fname); //new StringWriter());
instance.Completed = WorkflowCompleted; //流程完成事件
instance.OnUnhandledException = WorkflowUnhandledException; //流程异常处理
instance.Run();
NotifyG.Debug(fname + ":运行(开始)");
}
catch (Exception ex)
{
NotifyG.Error(ex.ToString());
return false;
}
return true;
}(4)流程追踪
它是一种调试功能,一方面可以记录每个活动的运行信息,另一方面可以定位当前正在运行的位置。
勾选主界面上的”工作流开启跟踪“,随便运行一个流程,比如”更新歌曲“。

可以看到设计器上会以黄色框提示你当前运行到了什么位置。
这种提示可以提醒你是否流程出错退出或者走的分支不是你想要的。
如果你想下断点让它停止下来,目前勇哥用的WF4.5还没这种功能。
注意,如果你是在VS中调试流程,是可以下断点的,但是像本例中,通过载入xmal的流程,则无法下断点。

(5)流程变量、活动的输入与输出参数
活动的输入与输出参数是活动专有的参数,对应流程中的InArgument与OutArgument。可以在宿主程序那边为该类型参数赋值。
而流程变量比较特殊,在宿主程序中要通过静态变量或者静态对象的方式进行赋值。

下面是活动”读取歌曲“的完整代码。
从中我们可以看到输入输出参数:
public InArgument<string> MusicFileDicPath { get; set; }
public OutArgument<TmusicList> OutMusicDicList {get;set;}其中MusicFileDicPath参数勇哥直接赋值了一个字符串常量。
输出参数OutMusicDicList则和输出参数outMusic绑定了起来,这个类型TmusicList还是个自定义的数据类型。
有关自定义数据类型做为参数的知识,请参见勇哥另一篇贴子《WF小练习:自定义变量和参数的数据类型》

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.ComponentModel;
using System.IO;
using System.Activities.Presentation.Metadata;
using UIShell.OSGi;
namespace Mtv.ToolKit.Help.commonFun
{
/// <summary>
/// 数据类型:歌曲列表
/// </summary>
public class TmusicList
{
public Dictionary<string, string> obj;
public TmusicList()
{
obj = new Dictionary<string, string>();
}
/// <summary>
/// 取歌曲名字列表
/// </summary>
/// <returns></returns>
public List<string> getMusicNameList()
{
var list = new List<string>();
foreach(var m in obj)
{
list.Add(m.Key);
}
return list;
}
public string toString()
{
var sb1 = new StringBuilder();
foreach(var m in obj)
{
sb1.Append(m.Key); sb1.Append(",");
sb1.Append(m.Value); sb1.Append(Environment.NewLine);
}
return sb1.ToString();
}
}
public sealed class readMusicFormDisk : CodeActivity
{
#region Field
private IHelpDevice helpDevice = null;
#endregion
#region 参数输入
[Category("01.参数输入")]
[DisplayName("歌曲目录路径串")]
[Description("输入路径串,string类型,C#字符串")]
public InArgument<string> MusicFileDicPath { get; set; }
private string musicFileDicPath=string.Empty;
#endregion
#region 参数输出
[Category("02.参数输出")]
[DisplayName("输入所有歌曲名和对应路径")]
[Description("输出歌曲名路径, TmusicList类型,C#")]
public OutArgument<TmusicList> OutMusicDicList {get;set;}
private TmusicList outMusicDicList = new TmusicList();
#endregion
public readMusicFormDisk()
{
AttributeTableBuilder builder = new AttributeTableBuilder();
builder.AddCustomAttributes(typeof(readMusicFormDisk), "DisplayName", new Attribute[1] { new BrowsableAttribute(false) });
MetadataStore.AddAttributeTable(builder.CreateTable());
DisplayName = "读取歌曲";
helpDevice = BundleRuntime.Instance.GetFirstOrDefaultService<IHelpDevice>() as IHelpDevice;
}
// 如果活动返回值,则从 CodeActivity<TResult>
// 派生并从 Execute 方法返回该值。
protected override void Execute(CodeActivityContext context)
{
NotifyG.Debug(DisplayName + "开始");
try
{
string text = context.GetValue(this.MusicFileDicPath);
musicFileDicPath = text;
readMusicFromDisk();
context.SetValue(OutMusicDicList, outMusicDicList);
}
catch(Exception ex)
{
Mtv.ToolKit.Help.NotifyG.Error(ex.Message);
}
}
private void readMusicFromDisk()
{
if (!Directory.Exists(musicFileDicPath))
{
throw new ArgumentException((string.Format("传入的歌曲目录[{0}]不存在!无法读取歌曲列表。", musicFileDicPath)));
}
Getall(new DirectoryInfo(musicFileDicPath));
}
private void Getall(DirectoryInfo dd)
{
FileInfo[] allfile = dd.GetFiles("*.*");
Array.Sort(allfile, delegate(FileInfo x, FileInfo y)
{
return x.CreationTime.CompareTo(y.CreationTime);
});
foreach (FileInfo tt in allfile)
{
var fileName = tt.Name.ToLower();
if (fileName.EndsWith(".mkv") || fileName.EndsWith(".mp3"))
{
outMusicDicList.obj.Add(tt.Name, tt.ToString());
}
}
DirectoryInfo[] direct = dd.GetDirectories();
foreach (DirectoryInfo dirTemp in direct)
{
Getall(dirTemp);
}
}
}
}在宿主程序中,通过如下代码的方式给输入参数listbox赋值。
private void btnUpdateMusic_Click(object sender, EventArgs e)
{
tabControl1.SelectedIndex = 0;
loadFlow(flowNameEnum.更新歌曲.ToString());
var dic = new Dictionary<string, object>();
dic.Add("listbox", this.lbxAllMusic);
WorkflowDesigner wd = factory.wd;
if (!cbxTrackingEnable.Checked) wd = null;
RunFlow(flowNameEnum.更新歌曲.ToString(),wd, dic);
}对于流程变量来说,没有直接在宿主程序为它赋值的语法。不过可以通过静态对象或者变量来为它传值进活动内部,
然后在活动内部用Assign活动来为流程变量赋值。
private void tbxSearchMusic_TextChanged(object sender, EventArgs e)
{
loadFlow(flowNameEnum.搜索歌曲.ToString());
//搜索文本
var txt = tbxSearchMusic.Text;
Config.Instance.allMusicListbox = lbxAllMusic;
Config.Instance.selMusicListbox = lbxSelectMusic;
Config.Instance.searchMusicName = txt;
Config.Instance.searchType = this.tabControl1.SelectedIndex == 0 ?
searchObjEnum.全部歌曲 : searchObjEnum.选中的歌曲;
Config.Instance.musicDicPath = "e:\\music";
WorkflowDesigner wd = factory.wd;
if (!cbxTrackingEnable.Checked) wd = null;
RunFlow(flowNameEnum.搜索歌曲.ToString(),wd);
timer1.Start();
}如下图所示,我们在流程内部用Assign为流程变量allMusicListbox赋值,这个赋的值为Config.Instance.allMusicListbox,正是由宿主程序传过来的静态对象的一个属性。

有关流程变量赋值的方式,详见勇哥另一篇贴子《在宿主程序中为流程变量赋值出错: 为根活动的参数提供的值不满足根活动的要求(宿主给流程变量传值的正确方式)》
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!


少有人走的路



















