使用 VSTO 2008 创建 Outlook 2007 表单区域






4.91/5 (21投票s)
在本文中,我们将创建一个窗体区域,用于显示同一类别内的所有联系人。
引言
在本文中,您将学习如何
- 构建 VSTO 2008 Outlook 插件
- 为您的 Outlook 联系人创建窗体区域
- 使用 Table、Column 和 Row 对象来访问和筛选 MAPIFolder 内容
- 检索联系人的图片
- 以编程方式启动即时消息会话
通过使用 VSTO 功能,您可以扩展/替换 Outlook 标准窗体,并用精美的 .NET 用户界面进行扩展。过去,您需要使用 Outlook Forms Designer 来自定义 Outlook 标准窗体。结果是出现一个没有 XP 风格的丑陋旧式窗体。使用 VSTO,您可以利用 Visual Studio 集成的窗体区域设计器来创建具有 .NET 控件的用户界面。
优点
- Outlook 窗体将保留新设计和新控件(如图片控件)
- 与您的 Visual Studio 设计器集成
- 代码模板和调试
概念
对于这个示例,我决定扩展 Outlook 标准联系人窗体,以显示一个包含同一类别内所有联系人的附加页面。在 Outlook 中,您可以为联系人、约会、邮件和任务分配类别。这些项目中的每一个都可以有多个类别,除了主类别列表之外,您还可以定义自己的类别。计划是获取当前联系人,获取其父文件夹,使用 Table 对象,然后查找与选定联系人属于同一类别的所有联系人。如果选定的联系人没有分配任何类别,则简单地显示文件夹中的所有联系人。
解决方案
在您可以创建此解决方案之前,您的开发计算机上需要具备以下先决条件。
必备组件
- Microsoft Outlook 2003 / Outlook 2007
- Office 主互操作程序集 (PIA) 2003 / 2007
- Microsoft Visual Studio 2008 和 VSTO 2008
创建解决方案
此示例是在 Vista Ultimate 64 位系统上使用 Outlook 2007 (德语) 和 Visual Studio 2008 (RTM) 构建的。启动您的 Visual Studio 并创建一个新项目。选择 Office/2007/Outlook Add-in 作为项目模板。
创建项目后,您会找到一个名为 ThisAddIn
的骨架类,其中有两个方法用于启动和终止应用程序。通常,在这里我会说——我们的应用程序在这里加载……——但这对于窗体区域来说是不正确的。仅供参考,请参阅下面的代码。
namespace MyContacts
{
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
ThisAddIn
类不应被修改。继续,让我们使用 Visual Studio 向导创建一个窗体区域。
创建窗体区域
只需右键单击您的项目并添加新项。选择 Outlook Form Region 作为模板。
在下一个屏幕截图中,您可以看到如何创建窗体区域。首先,选择 Design a new form region (设计一个新窗体区域)。
选择您想要哪种类型的窗体区域。对于此示例,请选择 Separate (分离) 类型,这意味着它将显示在一个额外的页面上。
为窗体区域命名。
最后,选择哪种类型的邮件类应该显示此窗体区域。在此示例中,仅支持联系人窗体。邮件类是 'IPM.Contact
'。
VSTO 附带的向导现在会为窗体区域创建一个代码模板。每次加载窗体区域时,都会调用 FormRegionShowing
方法。在窗体区域卸载之前,会执行 FormRegionClosed
方法。请参阅下面生成的桩方法。
#region Form Region Factory
[Microsoft.Office.Tools.Outlook.FormRegionMessageClass(
Microsoft.Office.Tools.Outlook.FormRegionMessageClassAttribute.Contact)]
[Microsoft.Office.Tools.Outlook.FormRegionName("MyContacts.FormRegionMyContacts")]
public partial class FormRegionMyContactsFactory {
// Occurs before the form region is initialized.
// To prevent the form region from appearing, set e.Cancel to true.
// Use e.OutlookItem to get a reference to the current Outlook item.
private void FormRegionMyContactsFactory_FormRegionInitializing(object sender,
Microsoft.Office.Tools.Outlook.FormRegionInitializingEventArgs e) {
}
}
#endregion
// Occurs before the form region is displayed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionShowing(object sender, System.EventArgs e) {
}
// Occurs when the form region is closed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionClosed(object sender, System.EventArgs e) {
}
现在,当您开始调试解决方案并打开一个联系人窗体时,您会在功能区中注意到一个额外的图标。通过此按钮,您可以访问新的窗体区域。
下面,您可以看到一个带有新按钮的联系人窗体。
在此示例中,我想显示与当前联系人属于同一类别的所有联系人。要实现此目标,您需要访问当前窗体区域的联系人项。在窗体区域类中,您可以通过 this.OutlookItem
访问当前的 Outlook 项。您有一个 ContactItem
类型的项,因此您必须显式地从通用 Outlook 项类型进行转换。联系人的类别可以通过 Categories
属性访问。该值是字符串的连接列表,以分号分隔。例如:当一个联系人被分配了“Outlook”和“Friends”类别时,Categories
的值将是“Outlook; Friends”。如果您想显示所有具有相同类别的其他联系人列表,您需要检索联系人的所需信息并在窗体区域中显示它。对于显示,您可以使用 ListView
,因为它易于使用,并且您还可以显示联系人图片。
您想要一个快速响应的应用程序,因此您可以使用 Table
对象来访问 MAPIfolder
的信息。可以使用某种 SQL 语法过滤 Table 的内容。因为您想检索同一类别的所有联系人,所以您需要为 Table
对象构建一个 SQL 过滤器。以下代码行向您展示了如何为类别构建过滤器
/// <summary>
/// Build the SQL Filter for the
/// </summary>
/// <param name="messageClass">The desired MessageClass</param>
/// <param name="categoryList">A list of categories for the contact</param>
/// <returns>returns the complete filter for the Table-Object</returns>
string BuildSQLFIlter(string messageClass, string[] categoryList) {
// We can use a StringBuilder object for
StringBuilder filter = new StringBuilder(250);
// SQL prefix
filter.Append(@"@SQL=(");
// only types with a specific messageClass
filter.Append(string.Format(@"(""http://schemas." +
"microsoft.com/mapi/proptag/0x001a001e"" = '{0}')",
messageClass ));
// are there categories ? append an AND conjunction
if (categoryList.Length > 0) filter.Append(" AND (");
// all categories of the List are ORed together
for (int index = 0; index < categoryList.Length; index++) {
if (index > 0) filter.Append(" OR ");
filter.Append(string.Format(@"(""urn:schemas-microsoft" +
"-com:office:office#Keywords"" LIKE '%{0}%')",
categoryList[index].Trim()));
}
// end bracket for the AND
if (categoryList.Length > 0) filter.Append(")");
// end bracket for the complete SQL
filter.Append(")");
return filter.ToString();
}
过滤器的含义是“所有消息类为“IPM.Contact”以及类别 A 或类别 B 或……”的所有类型。
当您访问过滤后的表项时,您必须告诉您想要检索哪些列。这是 MAPI 的本质,您想访问的列越多,从存储中检索数据所需的时间就越长。
您甚至无法通过 Table
对象直接访问图片。所以,这里最好的选择是尽快获取联系人数据,如姓名、电子邮件、主页等,而耗时操作应该在后台处理。当然,您会为此使用 Backgroundworker
对象。Backgroundworker
的工作是获取一个联系人项并从中提取联系人图片——然后将其传递给 ListView
中相应的项。
下面您可以看到用于检索所需数据的完整方法。
// Occurs before the form region is displayed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionShowing(
object sender, System.EventArgs e) {
Outlook.ContactItem contact;
Outlook.MAPIFolder folder;
Outlook.Table table;
try {
// get the current Outlook ContactItem
contact = this.OutlookItem as Outlook.ContactItem;
// the contact folder object
folder = contact.Parent as Outlook.MAPIFolder;
// retrieve the categories for the current contact
string categories = contact.Categories ?? string.Empty ;
string[] categoryList = categories.Split(';');
// build he SQL filter for the categories
string filter = BuildSQLFIlter ("IPM.Contact", categoryList );
// get the Table, filltered only for items matching
// the filter and the MessageClass, no HiddenItems
table = folder.GetTable(filter.ToString (),
Outlook.OlTableContents.olUserItems);
// define wich columns should be retrieved from table
table.Columns.RemoveAll();
table.Columns.Add("EntryID");
table.Columns.Add("FileAs");
table.Columns.Add(@"urn:schemas:contacts:profession");
table.Columns.Add("Email1Address");
table.Columns.Add(@"urn:schemas:contacts:businesshomepage");
// the propertytag for the Instant-Messenger address
table.Columns.Add(@"http://schemas.microsoft.com/mapi/" +
@"id/{00062004-0000-0000-C000-000000000046}/8062001F");
table.Columns.Add("Categories");
// the MAPI propertytag for the 'HasPicture' flag
table.Columns.Add(@"http://schemas.microsoft.com/mapi/" +
@"id/{04200600-0000-0000-C000-000000000046}/8015000B");
// create an Imagelist and add the 'NoPicture' image to the list
ImageList imageList = new ImageList();
imageList.ImageSize = IMAGESIZE;
imageList.ColorDepth = ColorDepth.Depth24Bit;
// assign the ImageList to the ListView in the form region
listViewContacts.LargeImageList = imageList;
// listViewContacts.TileSize = IMAGESIZE;
imageList.Images.Add(string.Empty , Properties.Resources.NoPicture);
// build a List of conatcs which have a picture assigned
List<string> contactsWithPicture = new List<string>();
// loop over the contacts and add them to the listView
// initially the businessCard is filled with the current Info
while (!table.EndOfTable) {
Outlook.Row row = table.GetNextRow();
// fill the COntactInfo from row information
ContactInfo info = new ContactInfo (row);
if (info.HasPicture) {
contactsWithPicture.Add(info.EntryId);
}
if (contact.EntryID != info.EntryId) {
ListViewItem listViewItem =
this.listViewContacts.Items.Add(info.FileAs, 0);
listViewItem.Tag = info;
listViewItem.Name = info.EntryId;
listViewItem.ToolTipText = info.EmailAddress;
} else {
// display the curent data in the businesscard view
UpdateBusinessCard(info, GetContactPicture(info.EntryId));
}
}
// the long running operation to retrieve
// the contact pictures is done in a separate thread
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.WorkerSupportsCancellation = true;
_backgroundWorker.DoWork += new DoWorkEventHandler(_backgroundWorker_DoWork);
// we pass along a list of contacts with attached pictures
_backgroundWorker.RunWorkerAsync(contactsWithPicture);
} finally {
table = null;
folder = null;
contact = null;
}
}
您了解到无法通过 Table
对象访问联系人图片。以下代码段演示了如何从 Outlook ContactItem
检索联系人图片。当联系人附有图片时,它可以作为普通附件访问,并且有一个默认名称,“ContactPicture.jpg”。
/// <summary>
/// retrieves the picture of a contact
/// </summary>
/// <param name="entryId">the entryId of the contact</param>
/// <returns>returns the image or null</returns>
private Image GetContactPicture(string entryId) {
// retrieve the contact item by it's entryID
Outlook.ContactItem contact =
Globals.ThisAddIn.Application.Session.GetItemFromID(entryId,
null) as Outlook.ContactItem;
Image img = null;
// a path to temporarily store the attached picture
string tempPath = Environment.GetEnvironmentVariable("TEMP");
if (contact != null) {
// check for the contact picture
foreach (Outlook.Attachment attachment in contact.Attachments) {
if (attachment.FileName == "ContactPicture.jpg") {
// save the file with a unique name
string fileName = Path.Combine(tempPath, entryId + ".jpg");
attachment.SaveAsFile(fileName);
// read the saved image into a Bitmap using a stream
FileStream stream = new FileStream(fileName,FileMode.Open );
Bitmap bmp = new Bitmap(Image.FromStream (stream,true ));
// check the aspect-ratio of the picture
if (bmp.Width >= bmp.Height) {
// scale Image
Bitmap tempBmp = new Bitmap(IMAGESIZE.Width ,IMAGESIZE.Height );
Graphics g = Graphics.FromImage (tempBmp );
g.FillRectangle(Brushes.White,0, 0, IMAGESIZE.Width, IMAGESIZE.Height);
float ratio = (float)bmp.Height / bmp.Width ;
int newHeight = (int)(ratio * bmp.Height);
// draw the scaled Image onto the empty Bitmap
int top = (IMAGESIZE.Height - newHeight) / 2;
g.DrawImage (bmp, new Rectangle (0,top, IMAGESIZE.Width , newHeight ));
img = tempBmp;
g.Dispose ();
} else {
// resize the picture
img = new Bitmap(bmp, IMAGESIZE);
}
stream.Dispose ();
// delete the temp file
File.Delete(fileName);
break;
}
}
}
contact = null;
return img;
}
对于每个有图片的联系人,图像将在后台工作线程中检索。线程应更新用户界面。通常,当从 UI 线程以外的线程访问控件时,您必须使用控件的 Invoke
方法。在下面的代码块中,您将看到图片如何从工作线程在窗体区域中更新。
/// <summary>
/// does all the background work
/// in this case, it get's the Images for the contacts and updates the imagelist
/// </summary>
/// <param name="sender">the backgroundworker-instace</param>
/// <param name="e">a startparameter which has been passed to the workerthread</param>
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
// we passed the list of contacts with assigned pictures
List<string> contactsWithPicture = (List<string>)e.Argument;
// loop over the list, and retrieve the
foreach (string entryId in contactsWithPicture) {
if (e.Cancel) break;
Image contactPicture = GetContactPicture(entryId);
if (contactPicture != null) {
SetImagePicture(entryId, contactPicture);
}
}
}
/// <summary>
/// defines a method signature used by the invoke command
/// </summary>
/// <param name="enrtyId">entryId of the contact</param>
/// <param name="image">image for the contact</param>
public delegate void SetImagePictureDelegate(string enrtyId, Image image);
/// <summary>
/// updates the contact picture in the listview
/// when this method is called from another thread - it will invoke itself
/// </summary>
/// <param name="enrtyId">entryId of the contact</param>
/// <param name="image">image for the contact</param>
public void SetImagePicture(string entryId, Image image) {
// if the listview needs invocation, call this
// method again using the listviews invoke method
if (listViewContacts.InvokeRequired) {
listViewContacts.Invoke(new SetImagePictureDelegate(
SetImagePicture), new object[] { entryId, image });
} else {
// if the listview has no item with
// the scpecific ID, everything is sensless
if (!listViewContacts.Items.ContainsKey(entryId)) return;
// get the index of the contactimage from the listview imagelist
int index = listViewContacts.LargeImageList.Images.IndexOfKey(entryId);
// when there is no such image, add it to the list
if (index == -1) {
listViewContacts.LargeImageList.Images.Add(image);
index = listViewContacts.LargeImageList.Images.Count-1;
} else {
listViewContacts.LargeImageList.Images[index] = image;
}
// tell the listviewItem that it should use the given image index
listViewContacts.Items[entryId].ImageIndex = index;
// get the index of the listviewitem and force a redraw
index = listViewContacts.Items[entryId].Index ;
listViewContacts.RedrawItems(index, index, false);
}
}
/// <summary>
/// backgroundworker is used to update the pictures of the contacts
/// </summary>
private BackgroundWorker _backgroundWorker;
// Occurs when the form region is closed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionClosed(object sender,
System.EventArgs e) {
if (_backgroundWorker.IsBusy) {
_backgroundWorker.CancelAsync();
}
_backgroundWorker.Dispose();
}
如上所示,当窗体区域卸载时,必须停止 BackgroundWorker
。其余的就是直接编码。只是为了稍微玩一下,我实现了一个小型名片,当鼠标悬停在列表中的联系人上时,会用联系人信息进行更新。双击列表项时,将在 Outlook 中显示相应的联系人。
/// <summary>
/// event sink for the listviewitem doubleclick.
/// used to open the selected contact.
/// </summary>
private void listViewContacts_DoubleClick(object sender, EventArgs e) {
Point position = listViewContacts.PointToClient(Control.MousePosition);
ListViewItem listViewItem =
listViewContacts.GetItemAt(position.X ,position.Y );
if (listViewItem == null) return;
OpenItem(listViewItem.Name);
}
/// <summary>
/// opens the contact
/// </summary>
/// <param name="entryId">entryId of the contact which shoul be displayed</param>
private void OpenItem(string entryId){
Outlook.ContactItem contact =
Globals.ThisAddIn.Application.Session.GetItemFromID(entryId, null)
as Outlook.ContactItem;
contact.Display(false);
contact = null;
}
/// <summary>
/// Occurs when the mouse hovers over an Item
/// </summary>
/// <param name="sender">The ListviewItem object</param>
/// <param name="e">The Item where the mouse is hovering</param>
private void listViewContacts_ItemMouseHover(object sender,
ListViewItemMouseHoverEventArgs e) {
UpdateBusinessCard((ContactInfo)e.Item.Tag,
listViewContacts.LargeImageList.Images[e.Item.ImageIndex]);
}
传递给名片的信息封装在一个简单的值持有者类 ContactInfo
中。这是 ContactInfo
类
public class ContactInfo {
/// <summary>
/// The constructor takes an Outlook Row
/// and parses the Information into the values
/// </summary>
/// <param name="row">the outlook row object</param>
public ContactInfo(Outlook.Row row) {
EntryId = (string)row[1];
FileAs = (string)row[2];
JobTitle = (string)(row[3] ?? string.Empty);
EmailAddress = (string)(row[4] ?? string.Empty);
Homepage = (string)(row[5] ?? string.Empty);
MessengerAddress = (string)(row[6] ?? string.Empty);
string categories = (string)(row[7] ?? string.Empty);
Categories = categories.Split(';');
HasPicture = (bool)(row[8] ?? false);
}
public string EntryId {get; private set; }
public string FileAs { get; private set; }
public string JobTitle { get; private set; }
public string EmailAddress { get; private set; }
public string Homepage { get; private set; }
public string MessengerAddress { get; private set; }
public string[] Categories { get; private set; }
public bool HasPicture { get; private set; }
}
如您所见,您可以像设计任何其他 .NET 控件一样,使用 Visual Studio 设计器设计窗体区域。
作为一项附加功能,您可以看到如何通过使用本机 COM API 使用 Windows Messenger(已在 MSN Live Messenger 上测试)向某人发送即时消息。您还可以单击“Email”并撰写一封新电子邮件,将该联系人作为收件人,或者在 Web 浏览器中显示其主页。您甚至可以双击名片并显示 Outlook 联系人。
请参阅下面的名片代码片段。
/// <summary>
/// fills the BusinessCard with the given ContactInfo
/// </summary>
/// <param name="info">the ContactInfo object.</param>
/// <param name="image" >the contact image</param>
public void SetContactInfo( ContactInfo info , Image image){
EntryId = info.EntryId;
LastFirst.Text = info.FileAs;
Profession.Text = info.JobTitle;
Emailaddress.Text = info.EmailAddress;
Messenger.Text = info.MessengerAddress;
Homepage.Text = info.Homepage;
Categories.Text = string.Join("\n", info.Categories);
Image.Image = image;
}
/// <summary>
/// The EntryId identifies the Outlook ContactItem
/// </summary>
public string EntryId { get; set; }
/// <summary>
/// event sink for the business card doubleclick event
/// </summary>
private void FormBusinessCard_MouseDoubleClick(object sender, MouseEventArgs e) {
OpenItem(EntryId);
}
/// <summary>
/// retrieves the contact from application context and displays it
/// </summary>
/// <param name="entryId">the entryId of the contact</param>
void OpenItem(string entryId) {
Outlook.ContactItem contact =
Globals.ThisAddIn.Application.Session.GetItemFromID (entryId ,
null) as Outlook.ContactItem ;
contact.Display(false);
contact = null;
}
/// <summary>
/// event sink for the homepage link clicked event
/// </summary>
private void Homepage_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
ShowHomePageInBrowser(Homepage.Text);
}
/// <summary>
/// displays the contacts homepage in a browser
/// </summary>
/// <param name="url">the homepage url</param>
void ShowHomePageInBrowser(string url){
Process p = new Process();
p.StartInfo.FileName = "IExplore.exe";
p.StartInfo.Arguments = url;
p.Start();
}
/// <summary>
/// event sink for the messenger lnk clicked event.
/// </summary>
private void Messenger_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
ConnectToMessenger(Messenger.Text);
}
/// <summary>
/// Takes the email from the InstantMessaging partner and opens the InstantMessage Window.
/// </summary>
/// <param name="email">the emailaddress of the partner</param>
void ConnectToMessenger(string email) {
try{
// create a COM - instance to the Messenger
Type messengerType = Type.GetTypeFromProgID ("Messenger.UIAutomation.1");
object comObject = Activator.CreateInstance (messengerType);
// call the InstantMessage method with the emailaddress of the user.
object[] arguments = new object[] { email };
messengerType.InvokeMember ("InstantMessage",
BindingFlags.InvokeMethod,null, comObject, arguments);
Marshal.ReleaseComObject(comObject);
} catch(System.Exception ex){
MessageBox.Show("Please make sure you have installed the" +
" latest Windows Messenger Live and that you are signed-in." );
}
}
/// <summary>
/// event sink for the email address link clicked event
/// </summary>
private void Emailaddress_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
SendEmail(LastFirst.Text, Emailaddress.Text);
}
/// <summary>
/// creates a new Email for the recipient using the emailaddress
/// </summary>
/// <param name="emailAddress">the contacts emailaddress</param>
void SendEmail(string name,string emailAddress) {
Outlook.MailItem mail = Globals.ThisAddIn.Application.CreateItem(
Microsoft.Office.Interop.Outlook.OlItemType.olMailItem) as Outlook.MailItem ;
mail.Recipients.Add (string.Format("{0}<{1}>", name, emailAddress ));
mail.To = emailAddress;
mail.Display(false);
mail = null;
}
目前就是这样。去玩玩窗体区域吧,看看使用 VSTO 技术和可视化设计器扩展 Outlook 窗体和用户界面是多么容易。一如既往——来自慕尼黑/德国的问候,希望您喜欢这篇文章。
恢复
- 构建 VSTO 2008 Outlook 插件
- 为您的 Outlook 联系人创建窗体区域
- 使用 Table、Column 和 Row 对象来访问和筛选 MAPIFolder 内容
- 检索联系人的图片
- 以编程方式启动即时消息会话
注释
以下注意事项适用于此以及所有 VSTO 插件
- 此解决方案的临时密钥是在我的开发计算机上创建的。您必须创建并使用自己的密钥。
- 该解决方案没有安装项目。有关分发 VSTO 插件的信息,请参阅 Deploying VSTO Solutions。
- 对于您的插件使用的每个 DLL,您都必须设置安全策略(MSI 安装包中的自定义操作)。
- VSTO 2008 Beta2 和 VSTO 2008 RTM 解决方案不兼容!!!您必须修补 .csproj 文件才能使其正常工作。
特别感谢(您将在他们的网站上找到大量关于使用和编程 Outlook、CDO 和 Exchange 的信息。没有他们的帮助,我不会是今天的我)
- Sue Mosher - www.outlookcode.com
- Ken Slovak - www.slovaktech.com
- Diane Poremsky - www.slipstick.com
- Robert Sparnaaij - www.howto-outlook.com
- 所有 MVP 和社区
历史
- V.1.0 - 初始版本(2007 年 12 月 23 日)
- V.1.1 - 添加了 VB.NET 解决方案(2008 年 1 月 1 日)