[源码下载]

重新想象 Windows 8 Store Apps (68) - 后台任务: 控制通道(ControlChannel)

作者:webabcd

介绍
重新想象 Windows 8 Store Apps 之 后台任务

  • 控制通道(ControlChannel)

示例
1、客户端与服务端做 ControlChannel 通信的关键代码
ControlChannelHelper/AppContext.cs

/*
* 本例通过全局静态变量来实现 app 与 task 的信息共享,以便后台任务可以获取到 app 中的相关信息
*
* 注:
* 也可以通过 Windows.ApplicationModel.Core.CoreApplication.Properties 保存数据,以实现 app 与 task 的信息共享
*/ using System.Collections.Concurrent;
using Windows.Networking.Sockets; namespace ControlChannelHelper
{
public class AppContext
{
/// <summary>
/// 从 ControlChannel 接收到的数据
/// </summary>
public static ConcurrentQueue<string> MessageQueue = new ConcurrentQueue<string>(); /// <summary>
/// 客户端 socket
/// </summary>
public static StreamSocket ClientSocket;
}
}

ControlChannelHelper/SocketControlChannel.cs

/*
* 实现一个 socket tcp 通信的 ControlChannel,client 将在此 ControlChannel 中实时接收数据
*
* 注:
* win8 client 和 socket server 不能部署在同一台机器上,否则会抛出异常:{参考的对象类型不支持尝试的操作。 (异常来自 HRESULT:0x8007273D)}
*/ using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.Background;
using Windows.Foundation;
using Windows.Networking;
using Windows.Networking.Sockets;
using Windows.Storage.Streams; namespace ControlChannelHelper
{
public class SocketControlChannel : IDisposable
{
// ControlChannel
public ControlChannelTrigger Channel { get; set; } // 客户端 socket
private StreamSocket _socket;
// 用于发送数据
private DataWriter _dataWriter;
// 用于接收数据
private DataReader _dataReader; // 向服务端发送心跳的间隔时间,单位为分钟,最小 15 分钟
private uint _serverKeepAliveInterval = ;
// ControlChannel 的标识
private string _channelId = "myControlChannel"; public SocketControlChannel()
{ } public async Task<string> CreateChannel()
{
Dispose(); try
{
// 实例化一个 ControlChannel
Channel = new ControlChannelTrigger(_channelId, _serverKeepAliveInterval, ControlChannelTriggerResourceType.RequestHardwareSlot);
}
catch (Exception ex)
{
Dispose();
return "控制通道创建失败:" + ex.ToString();
} // 注册用于向服务端 socket 发送心跳的后台任务,需要在 manifest 中做相关配置
var keepAliveBuilder = new BackgroundTaskBuilder();
keepAliveBuilder.Name = "myControlChannelKeepAlive";
// 注:如果走的是 WebSocket 协议,则系统已经为其内置了发送心跳的逻辑,此处直接指定为 Windows.Networking.Sockets.WebSocketKeepAlive 即可
keepAliveBuilder.TaskEntryPoint = "BackgroundTaskLib.ControlChannelKeepAlive";
keepAliveBuilder.SetTrigger(Channel.KeepAliveTrigger); // 到了发送心跳的间隔时间时则触发,本例是 15 分钟
keepAliveBuilder.Register(); // 注册用于向用户显示通知的后台任务,需要在 manifest 中做相关配置
var pushNotifyBuilder = new BackgroundTaskBuilder();
pushNotifyBuilder.Name = "myControlChannelPushNotification";
pushNotifyBuilder.TaskEntryPoint = "BackgroundTaskLib.ControlChannelPushNotification";
pushNotifyBuilder.SetTrigger(Channel.PushNotificationTrigger); // 在 ControlChannel 中收到了推送过来的数据时则触发
pushNotifyBuilder.Register(); try
{
_socket = new StreamSocket();
AppContext.ClientSocket = _socket; // 在 ControlChannel 中通过指定的 StreamSocket 通信
Channel.UsingTransport(_socket); // client socket 连接 server socket
await _socket.ConnectAsync(new HostName("192.168.6.204"), ""); // 开始等待 ControlChannel 中推送过来的数据,如果 win8 client 和 socket server 部署在同一台机器上,则此处会抛出异常
ControlChannelTriggerStatus status = Channel.WaitForPushEnabled(); if (status != ControlChannelTriggerStatus.HardwareSlotAllocated && status != ControlChannelTriggerStatus.SoftwareSlotAllocated)
return "控制通道创建失败:" + status.ToString(); // 发送数据到服务端
_dataWriter = new DataWriter(_socket.OutputStream);
string message = "hello " + DateTime.Now.ToString("hh:mm:ss") + "^";
_dataWriter.WriteString(message);
await _dataWriter.StoreAsync(); // 接收数据
ReceiveData();
}
catch (Exception ex)
{
Dispose();
return "控制通道创建失败:" + ex.ToString();
} return "ok";
} // 开始接收此次数据
private void ReceiveData()
{
uint maxBufferLength = ; try
{
var buffer = new Windows.Storage.Streams.Buffer(maxBufferLength);
var asyncOperation = _socket.InputStream.ReadAsync(buffer, maxBufferLength, InputStreamOptions.Partial);
asyncOperation.Completed = (IAsyncOperationWithProgress<IBuffer, uint> asyncInfo, AsyncStatus asyncStatus) =>
{
switch (asyncStatus)
{
case AsyncStatus.Completed:
case AsyncStatus.Error:
try
{
IBuffer bufferRead = asyncInfo.GetResults();
uint bytesRead = bufferRead.Length;
_dataReader = DataReader.FromBuffer(bufferRead); // 此次数据接收完毕
ReceiveCompleted(bytesRead);
}
catch (Exception ex)
{
AppContext.MessageQueue.Enqueue(ex.ToString());
}
break;
case AsyncStatus.Canceled:
AppContext.MessageQueue.Enqueue("接收数据时被取消了");
break;
}
};
}
catch (Exception ex)
{
AppContext.MessageQueue.Enqueue(ex.ToString());
}
} public void ReceiveCompleted(uint bytesRead)
{
// 获取此次接收到的数据
uint bufferLength = _dataReader.UnconsumedBufferLength;
string message = _dataReader.ReadString(bufferLength); // 将接收到的数据放到内存中,由 PushNotificationTrigger 触发的后台任进行处理(当然也可以在此处处理)
AppContext.MessageQueue.Enqueue(message); // 开始接收下一次数据
ReceiveData();
} // 释放资源
public void Dispose()
{
lock (this)
{
if (_dataWriter != null)
{
try
{
_dataWriter.DetachStream();
_dataWriter = null;
}
catch (Exception ex)
{ }
} if (_dataReader != null)
{
try
{
_dataReader.DetachStream();
_dataReader = null;
}
catch (Exception exp)
{ }
} if (_socket != null)
{
_socket.Dispose();
_socket = null;
} if (Channel != null)
{
Channel.Dispose();
Channel = null;
}
}
}
}
}

2、客户端辅助类
BackgroundTaskLib/ControlChannelKeepAlive.cs

/*
* 用于向服务端 socket 发送心跳的后台任务
*
* 注:
* 如果走的是 WebSocket 协议,则系统已经为其内置了发送心跳的逻辑
* 只需要将 BackgroundTaskBuilder.TaskEntryPoint 设置为 Windows.Networking.Sockets.WebSocketKeepAlive 即可,而不需要再自定义此后台任务
*/ using ControlChannelHelper;
using System;
using Windows.ApplicationModel.Background;
using Windows.Networking.Sockets;
using Windows.Storage.Streams; namespace BackgroundTaskLib
{
public sealed class ControlChannelKeepAlive : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
if (taskInstance == null)
return; // 获取 ControlChannel
var channelEventArgs = taskInstance.TriggerDetails as IControlChannelTriggerEventDetails;
ControlChannelTrigger channel = channelEventArgs.ControlChannelTrigger; if (channel == null)
return; string channelId = channel.ControlChannelTriggerId; // 发送心跳
SendData();
} private async void SendData()
{
// 发送心跳到 server socket
DataWriter dataWriter = new DataWriter(AppContext.ClientSocket.OutputStream);
string message = "hello " + DateTime.Now.ToString("hh:mm:ss") + "^";
dataWriter.WriteString(message);
await dataWriter.StoreAsync();
}
}
}

BackgroundTaskLib/ControlChannelPushNotification.cs

/*
* 用于向用户显示通知的后台任务,需要在 manifest 中做相关配置
*/ using ControlChannelHelper;
using NotificationsExtensions.ToastContent;
using System;
using Windows.ApplicationModel.Background;
using Windows.Networking.Sockets;
using Windows.UI.Notifications; namespace BackgroundTaskLib
{
public sealed class ControlChannelPushNotification : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
if (taskInstance == null)
return; // 获取 ControlChannel
var channelEventArgs = taskInstance.TriggerDetails as IControlChannelTriggerEventDetails;
ControlChannelTrigger channel = channelEventArgs.ControlChannelTrigger; if (channel == null)
return; string channelId = channel.ControlChannelTriggerId; try
{
string messageReceived; // 将从 ControlChannel 中接收到的信息,以 toast 的形式弹出
while (AppContext.MessageQueue.Count > )
{
bool result = AppContext.MessageQueue.TryDequeue(out messageReceived);
if (result)
{
IToastText01 templateContent = ToastContentFactory.CreateToastText01();
templateContent.TextBodyWrap.Text = messageReceived;
templateContent.Duration = ToastDuration.Short;
IToastNotificationContent toastContent = templateContent;
ToastNotification toast = toastContent.CreateNotification(); ToastNotifier toastNotifier = ToastNotificationManager.CreateToastNotifier();
toastNotifier.Show(toast);
}
}
}
catch (Exception ex)
{ }
}
}
}

3、客户端
BackgroundTask/ControlChannel.xaml

<Page
x:Class="XamlDemo.BackgroundTask.ControlChannel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:XamlDemo.BackgroundTask"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"> <Grid Background="Transparent">
<StackPanel Margin="120 0 0 0"> <TextBlock Name="lblMsg" FontSize="14.667" /> <Button Name="btnCreateChannel" Content="创建一个 ControlChannel" Margin="0 10 0 0" Click="btnCreateChannel_Click" /> </StackPanel>
</Grid>
</Page>

BackgroundTask/ControlChannel.xaml.cs

/*
* 演示如何创建一个基于 socket tcp 通信的 ControlChannel,client 将在此 ControlChannel 中实时接收数据
*
* 注:
* 不能在模拟器中运行
* RTC - Real Time Communication 实时通信
* win8 client 和 socket server 不能部署在同一台机器上,否则会抛出异常:{参考的对象类型不支持尝试的操作。 (异常来自 HRESULT:0x8007273D)}
*/ using System;
using ControlChannelHelper;
using Windows.ApplicationModel.Background;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Popups; namespace XamlDemo.BackgroundTask
{
public sealed partial class ControlChannel : Page
{
public ControlChannel()
{
this.InitializeComponent();
} private async void btnCreateChannel_Click(object sender, RoutedEventArgs e)
{
// 如果 app 在锁屏上,则可以通过 ControlChannelTrigger 触发指定的后台任务
BackgroundAccessStatus status = BackgroundExecutionManager.GetAccessStatus();
if (status == BackgroundAccessStatus.Unspecified)
{
status = await BackgroundExecutionManager.RequestAccessAsync();
}
if (status == BackgroundAccessStatus.Denied)
{
await new MessageDialog("请先将此 app 添加到锁屏").ShowAsync();
return;
} // 创建一个基于 socket tcp 通信的 ControlChannel,相关代码参见:ControlChannelHelper 项目
SocketControlChannel channel = new SocketControlChannel();
string result = await channel.CreateChannel(); lblMsg.Text = result;
}
}
}

4、服务端
SocketServerTcp/ClientSocketPacket.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace SocketServerTcp
{
/// <summary>
/// 对客户端 Socket 及其他相关信息做一个封装
/// </summary>
public class ClientSocketPacket
{
/// <summary>
/// 客户端 Socket
/// </summary>
public System.Net.Sockets.Socket Socket { get; set; } private byte[] _buffer;
/// <summary>
/// 为该客户端 Socket 开辟的缓冲区
/// </summary>
public byte[] Buffer
{
get
{
if (_buffer == null)
_buffer = new byte[]; return _buffer;
}
} private List<byte> _receivedByte;
/// <summary>
/// 客户端 Socket 发过来的信息的字节集合
/// </summary>
public List<byte> ReceivedByte
{
get
{
if (_receivedByte == null)
_receivedByte = new List<byte>(); return _receivedByte;
}
}
}
}

SocketServerTcp/Main.cs

/*
* 从以前写的 wp7 demo 中直接复制过来的,用于演示如何通过 ControlChannel 实时地将信息以 socket tcp 的方式推送到 win8 客户端
*
* 注:
* 本例通过一个约定结束符来判断是否接收完整,其仅用于演示,实际项目中请用自定义协议。可参见:XamlDemo/Communication/TcpDemo.xaml.cs
*/ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms; using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO; namespace SocketServerTcp
{
public partial class Main : Form
{
SynchronizationContext _syncContext; System.Timers.Timer _timer; // 信息结束符,用于判断是否完整地读取了客户端发过来的信息,要与客户端的信息结束符相对应(本例只用于演示,实际项目中请用自定义协议)
private string _endMarker = "^"; // 服务端监听的 socket
private Socket _listener; // 实例化 ManualResetEvent,设置其初始状态为无信号
private ManualResetEvent _signal = new ManualResetEvent(false); // 客户端 Socket 列表
private List<ClientSocketPacket> _clientList = new List<ClientSocketPacket>(); public Main()
{
InitializeComponent(); // UI 线程
_syncContext = SynchronizationContext.Current; // 启动后台线程去运行 Socket 服务
Thread thread = new Thread(new ThreadStart(LaunchSocketServer));
thread.IsBackground = true;
thread.Start();
} private void LaunchSocketServer()
{
// 每 10 秒运行一次计时器所指定的方法,群发信息
_timer = new System.Timers.Timer();
_timer.Interval = 10000d;
_timer.Elapsed += new System.Timers.ElapsedEventHandler(_timer_Elapsed);
_timer.Start(); // TCP 方式监听 3366 端口
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_listener.Bind(new IPEndPoint(IPAddress.Any, ));
// 指定等待连接队列中允许的最大数
_listener.Listen(); while (true)
{
// 设置为无信号
_signal.Reset(); // 开始接受客户端传入的连接
_listener.BeginAccept(new AsyncCallback(OnClientConnect), null); // 阻塞当前线程,直至有信号为止
_signal.WaitOne();
}
} private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// 每 10 秒给所有连入的客户端发送一次消息
SendData(string.Format("webabcd 对所有人说:大家好! 【信息来自服务端 {0}】", DateTime.Now.ToString("hh:mm:ss")));
} private void OnClientConnect(IAsyncResult async)
{
ClientSocketPacket client = new ClientSocketPacket();
// 完成接受客户端传入的连接的这个异步操作,并返回客户端连入的 socket
client.Socket = _listener.EndAccept(async); // 将客户端连入的 Socket 放进客户端 Socket 列表
_clientList.Add(client); OutputMessage(((IPEndPoint)client.Socket.LocalEndPoint).Address + " 连入了服务器");
SendData("一个新的客户端已经成功连入服务器。。。 【信息来自服务端】"); try
{
// 开始接收客户端传入的数据
client.Socket.BeginReceive(client.Buffer, , client.Buffer.Length, SocketFlags.None, new AsyncCallback(OnDataReceived), client);
}
catch (SocketException ex)
{
// 处理异常
HandleException(client, ex);
} // 设置为有信号
_signal.Set();
} private void OnDataReceived(IAsyncResult async)
{
ClientSocketPacket client = async.AsyncState as ClientSocketPacket; int count = ; try
{
// 完成接收数据的这个异步操作,并返回接收的字节数
if (client.Socket.Connected)
count = client.Socket.EndReceive(async);
}
catch (SocketException ex)
{
HandleException(client, ex);
} // 把接收到的数据添加进收到的字节集合内
// 本例采用 UTF8 编码,中文占用 3 字节,英文等字符与 ASCII 相同
foreach (byte b in client.Buffer.Take(count))
{
if (b == ) continue; // 如果是空字节则不做处理('\0') client.ReceivedByte.Add(b);
} // 把当前接收到的数据转换为字符串。用于判断是否包含自定义的结束符
string receivedString = UTF8Encoding.UTF8.GetString(client.Buffer, , count); // 如果该 Socket 在网络缓冲区中没有排队的数据 并且 接收到的数据中有自定义的结束符时
if (client.Socket.Connected && client.Socket.Available == && receivedString.Contains(_endMarker))
{
// 把收到的字节集合转换成字符串(去掉自定义结束符)
// 然后清除掉字节集合中的内容,以准备接收用户发送的下一条信息
string content = UTF8Encoding.UTF8.GetString(client.ReceivedByte.ToArray());
content = content.Replace(_endMarker, "");
client.ReceivedByte.Clear(); // 发送数据到所有连入的客户端,并在服务端做记录
SendData(content);
OutputMessage(content);
} try
{
// 继续开始接收客户端传入的数据
if (client.Socket.Connected)
client.Socket.BeginReceive(client.Buffer, , client.Buffer.Length, , new AsyncCallback(OnDataReceived), client);
}
catch (SocketException ex)
{
HandleException(client, ex);
}
} /// <summary>
/// 发送数据到所有连入的客户端
/// </summary>
/// <param name="data">需要发送的数据</param>
private void SendData(string data)
{
byte[] byteData = UTF8Encoding.UTF8.GetBytes(data); foreach (ClientSocketPacket client in _clientList)
{
if (client.Socket.Connected)
{
try
{
// 如果某客户端 Socket 是连接状态,则向其发送数据
client.Socket.BeginSend(byteData, , byteData.Length, SocketFlags.None, new AsyncCallback(OnDataSent), client);
}
catch (SocketException ex)
{
HandleException(client, ex);
}
}
else
{
// 某 Socket 断开了连接的话则将其关闭,并将其清除出客户端 Socket 列表
// 也就是说每次向所有客户端发送消息的时候,都会从客户端 Socket 集合中清除掉已经关闭了连接的 Socket
client.Socket.Close();
_clientList.Remove(client);
}
}
} private void OnDataSent(IAsyncResult async)
{
ClientSocketPacket client = async.AsyncState as ClientSocketPacket; try
{
// 完成将信息发送到客户端的这个异步操作
int sentBytesCount = client.Socket.EndSend(async);
}
catch (SocketException ex)
{
HandleException(client, ex);
}
} /// <summary>
/// 处理 SocketException 异常
/// </summary>
/// <param name="client">导致异常的 ClientSocketPacket</param>
/// <param name="ex">SocketException</param>
private void HandleException(ClientSocketPacket client, SocketException ex)
{
// 在服务端记录异常信息,关闭导致异常的 Socket,并将其清除出客户端 Socket 列表
OutputMessage(client.Socket.RemoteEndPoint.ToString() + " - " + ex.Message);
client.Socket.Close();
_clientList.Remove(client);
} // 在 UI 上输出指定信息
private void OutputMessage(string data)
{
_syncContext.Post((p) => { txtMsg.Text += p.ToString() + "\r\n"; }, data);
}
}
}

OK
[源码下载]

重新想象 Windows 8 Store Apps (68) - 后台任务: 控制通道(ControlChannel)的更多相关文章

  1. 重新想象 Windows 8 Store Apps (64) - 后台任务: 开发一个简单的后台任务

    [源码下载] 重新想象 Windows 8 Store Apps (64) - 后台任务: 开发一个简单的后台任务 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 后 ...

  2. 重新想象 Windows 8 Store Apps (65) - 后台任务: 音乐的后台播放和控制

    [源码下载] 重新想象 Windows 8 Store Apps (65) - 后台任务: 音乐的后台播放和控制 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 后台 ...

  3. 重新想象 Windows 8 Store Apps (66) - 后台任务: 下载和上传

    [源码下载] 重新想象 Windows 8 Store Apps (66) - 后台任务: 下载和上传 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 后台任务 后台 ...

  4. 重新想象 Windows 8 Store Apps (67) - 后台任务: 推送通知

    [源码下载] 重新想象 Windows 8 Store Apps (67) - 后台任务: 推送通知 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 后台任务 推送通 ...

  5. 重新想象 Windows 8 Store Apps 系列文章索引

    [源码下载][重新想象 Windows 8.1 Store Apps 系列文章] 重新想象 Windows 8 Store Apps 系列文章索引 作者:webabcd 1.重新想象 Windows ...

  6. 重新想象 Windows 8 Store Apps (48) - 多线程之其他辅助类: SpinWait, SpinLock, Volatile, SynchronizationContext, CoreDispatcher, ThreadLocal, ThreadStaticAttribute

    [源码下载] 重新想象 Windows 8 Store Apps (48) - 多线程之其他辅助类: SpinWait, SpinLock, Volatile, SynchronizationCont ...

  7. 重新想象 Windows 8 Store Apps (59) - 锁屏

    [源码下载] 重新想象 Windows 8 Store Apps (59) - 锁屏 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 锁屏 登录锁屏,获取当前程序的锁 ...

  8. 重新想象 Windows 8 Store Apps (61) - 通信: http, oauth

    [源码下载] 重新想象 Windows 8 Store Apps (61) - 通信: http, oauth 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 通信 ...

  9. 重新想象 Windows 8 Store Apps (34) - 通知: Toast Demo, Tile Demo, Badge Demo

    [源码下载] 重新想象 Windows 8 Store Apps (34) - 通知: Toast Demo, Tile Demo, Badge Demo 作者:webabcd 介绍重新想象 Wind ...

随机推荐

  1. Three levels at which any machine carrying out an Information-Processing task must be understood

    1. Computational theory What is the goal of computation, why is it appropriate, and what is the logi ...

  2. Lists in Prolog

    Symbols in Prolog: atom variable number list (how to assembly and take them apart)   Lists are very  ...

  3. MariaDB Galera Cluster 10.1 只支持 LINUX ?!

    MariaDB Galera Cluster  (MariaDB 10.1) 当前只支持:LINUX ! 参考: https://mariadb.com/kb/en/mariadb/getting-s ...

  4. windows内核结构

  5. Python--Cmd窗口运行Python时提示Fatal Python error: Py_Initialize: can't initialize sys standard streams LookupError: unknown encoding: cp65001

    源地址连接: http://www.tuicool.com/articles/ryuaUze 最近,我在把一个 Python 2 的视频下载工具 youku-lixian 改写成 Python 3,并 ...

  6. 原生DOM探究 -- NodeList v.s. HTMLCollection

    涉及获取元素的主要API 在获取原生DOM元素的时候,主要涉及这几个DOM API(链接为Living Standard): Node及对应集合NodeList Element(继承Node)及对应集 ...

  7. 六款值得推荐的android(安卓)开源框架

    1.volley 项目地址 https://github.com/smanikandan14/Volley-demo (1)  JSON,图像等的异步下载: (2)  网络请求的排序(scheduli ...

  8. 另类angularjs应用

    回顾 上一篇文章主要讲解了创建兼容任意浏览器(主要是ie的一些奇葩问题)的angularjs web应用,但是项目开发中其实更重要的还是在功能的模块化.代码自动压缩上面,这样项目在后期维护或者功能的重 ...

  9. Java Struts2读取Excel 2003/2007/2010例子

    Java读写Excel的包是Apache POI(项目地址:http://poi.apache.org/),因此需要先获取POI的jar包,本实验使用的是POI 3.9稳定版. Apache POI ...

  10. 检索 COM 类工厂中 CLSID 为 {13C28AD0-F195-4319-B7D7-A1BDAA329FB8} 的组件时失败,原因是出现以下错误: 80040154

    异常类型:COMException 异常消息:检索 COM 类工厂中 CLSID 为 {13C28AD0-F195-4319-B7D7-A1BDAA329FB8} 的组件时失败,原因是出现以下错误: ...