在线相册

来源:互联网 发布:淘宝上的丝芙兰旗舰店 编辑:程序博客网 时间:2024/04/26 06:31

An ASP.NET Application to View and Share Photos Online

一个在线观看和共共享相片的Asp.net程序
By Greg Ennis

This article explains an ASP.NET application to view and share photos online. 

这篇文章解释一个在线观看和共享相片的Asp.net的程序。

最后的效果如下:

 

 

Introduction

介绍

This application gives you some basic photo-sharing capability similar to Ofoto or Yahoo-photos. The advantage to rolling your own application is that you gain full control over the content and layout of your site (no popup ads!) and you can completely customize it to fit your needs.

这个程序展示给你一些基本的相片共享能力,像Ofoto或者Yahoo-photos一样。而你自己程序的优点就是你可以在你的WEB站点上对内容及版面进行完全的控制,而且你也能够根据你的需求来定制它.

This article describes the application, which is actually 2 separate apps - a back-end Windows Forms C# application that scans your directories and files to build a database, and an ASP.NET application that presents the photos and allows the user to view them and edit them (to provide a caption and description).

这篇文章描述这个应用程序,准确的说是两个分开的程序。一个是后台用来扫描你目录和文件用来建立数据库的Windows Formc#应用程序,一个是展示相片并允许用户浏览和编辑它们(加标题和描述)的ASP.net的前端浏览程序(。

Note that there is a C# Photo Album Viewer posted on CodeProject by Mark Nischalke, but that is Windows Forms only. I was looking for something enabled for use through a web browser.

要注意的是在CodeProject上已经有一个Mark Nischalke 发布的C#版本的相册浏览器,但是那个仅仅是运行在Windwos Form上。我正在找的是一些能够通过Web浏览器使用东西。

Background

背景

This article assume you have basic working knowledge of C#, Windows Forms, ASP.NET programming, and some SQL statement knowledge. You'll need to have either the full version of SQL Server, or you can get MSDE, the free version of SQL.

这篇文章假设你已经有基本的C#语言,Windows Form,以及一些Asp.net的编程知识。并且懂一些基本的SQL基本语句知道。同时你或者你完整版本的Sql Server数据库,或者是免费的Sql Server数据库MSDE

The application as it is currently built will scan for all *.jpg files in subdirectories of your pictures folder. Future enhancements may include recursively scanning all files in all subdirectories.

目前编译的这个版本只是扫描子目录下面所有的*.jpg的文件,将来将会增强,会增加递归扫描所有子目录下的jpg文件。

Using the code

使用的代码

Before reading the article you may want to get the code installed and running. To do this you should follow these steps:

在你阅读这篇文件之前你可能想得到安装和运行的代码,按如下的步骤:

  1. Download the Back-end Windows Forms app and unzip it somewhere on your hard drive. (下载后端的Windows Forms程序,解压后放在你硬盘的某个地方)

  2. Edit the application config file to specify your SQL server and the root folder containing your pictures. This file is called App.Config, but note that VS.NET will automatically copy and rename this file to NPdbman.exe.config when you build the project. (编辑程序的Config配置文件来指向你的Sql Server数据库和包含你图片的根目录。这个文件叫App.Config。但是要注意的是当你在新建这个工程的时候,VS.net的开发环境会自动拷贝并且重命名这个文件为你的应用程序名.Config.

  3. Run the NPdbman.exe application and select "Initialize" from the database menu. This will create the tables and constraints in a new SQL database called netpix. (运行Npdbman.exe程序并且从数据库菜单中选择”Initialize”,这将会在一个叫netpix的新数据库里创建表和结束。

  4. Select "Build" from the database menu. This will populate the tables with the information scanned from the folder specified in the configuration file. (选择数据库菜单中的编译(build)。这将会把一些从指定的配置文件目录中信息扫描移入数据库。

  5. Download the Front-end ASP.NET app and unzip it in the IIS wwwroot folder. Run the IIS configuration tool, and right-click on the netpix folder. Select 'Properties' and in the 'Application Settings' pane, select 'Create'. (下载前端的Asp.net程序,并把它解压在IISwwwroot文件夹中,然后运行IIS配置工具,然后在netpix文件夹上右击,选择属性,在应用程序设置面板中选择创建。

  6. Edit the web.config for the application and specify your SQL server connection. 编辑文件的web.config配置文件指定你的Sql Server数据库位置。

  7. You should now be able to browse to http://localhost/netpix/default.aspx and browse your photos! (现在你就可以在浏览器中输入地址http://localhost/netpix/default.aspx来浏览你的相册啦)

The database

数据库

It would have certainly been possible to build a simple application that did not use a database and simply scanned the folder and file information on the fly, but using a database will allow us to implement some advanced features which would have been awkward and difficult without the power of the RDBMS.

对于建立一个不使用数据库而仅仅扫描文件夹和文件信息的程序来说是完全可能实现的,但是使用数据库让我们来实现一些没有强有力的关系数据库支持很难实现的其它的高级功能。

Schema

架构

Here is a diagram of the database, which consists of just 2 small related tables:

下图就是数据库的关系图,它仅仅包含两下相关的表。

 

The albums table is built from subdirectories of your Pictures folder. Each subdirectory maps to one album in the table. You can provide a description for an album, so that the user will see a name for the album which may or may not be the actual folder name in the file system.

Albums表用来存放你相片所在文件夹所以的子目录,每个子目录对应表里面的一个相册,你也可以增加对这个相册的描述,以便用户看到这个相册名字而不仅仅是一个系统文件夹的名字。

The pics table is built from the *.jpg files found in each subdirectory (album). The pics table is related to the albums table by a foreign key constraint, because every picture must belong to an album. Most of the column names should be pretty self-explanatory except perhaps numviews. This column counts the number of times a user has viewed the full picture and is incremented by the ASP.NET code, every time that a user clicks on a picture. We will see this code shortly.

pics用来存放每个子目录里(album)表里的.jpg文件。这个表通过一个外键约束与表albums相关。因为每一张相片必须属于一个相册,除了numviews这个字段外,其它大部分的字段都顾名思义,比较好解释,numviews这个字段存来存放用户浏览浏览完整相片的次数,当用户每次单击一张图片的时候,由asp.net的代码进行增量操作,不久后我们将看到这段代码。

Stored procedures

存贮过程

There are just a couple stored procedures. The first one inserts a picture into the pics table (if it does not already exist there):

系统只用到两个存贮过程,一个是插入一幅图片到表pics里面(如果并不存在这条记录的话):

CREATE PROCEDURE CreatePic(@albumid int, @filename varchar(255), 
                           @width int, @height int, 
                           @imgdate datetime, @imgsize int) AS

 

IF NOT EXISTS(SELECT [id] FROM pics WHERE albumid=@albumid 
  AND [filename]=@filename)
  INSERT INTO pics (albumid, [filename], width, 
            height, imgdate, imgsize) 
         values (@albumid, @filename, @width, 
            @height, @imgdate, @imgsize);

The second is similar, but it operates on the albums table and it returns the identity value of the new record (or the existing one):

第二个存贮程序也很相似,但是它是在表albums操作,并且返回新记录的(或者是存在记录的)键值。

CREATE PROCEDURE CreateAlbum(@rootpath varchar(1024), 
                        @description varchar(255), @id int output) AS

 

SELECT @id = (SELECT [id] FROM albums WHERE rootpath=@rootpath);

 

IF @id IS NULL
BEGIN
  INSERT INTO albums (rootpath, [description]) 
                values (@rootpath, @description);
  SET @id = SCOPE_IDENTITY();
END

The back-end

后端

The back end is a Windows Forms application that you run on the server to build the database by scanning your pictures folder. There are 2 basic functions: to reset (delete) everything in the database, and then to scan and build all the entries. This article won't discuss how you build a forms application or hook up menu entries, etc., because that is covered in depth elsewhere.

后端是一个运行在服务器上面的用来扫描相片目录来建立数据库的windows form的应用程序,有两个基本功能,重置(删除)数据库里面的所以数据,然后扫描并构建所有的个体,这篇文章不会讨论如何建立这个程序或者挂菜单等等,因为那会被涉及在其它深一些的文章。

Reset/ Initialize Code

重置和初始化代码

Let's take a look at the reset/initialize code. First there is a generic routine which reads a .sql script file and executes it on the given connection:

让我们看一下初置或者是初始化的代码,首先是一段读取一个sql脚本文件并在已经找开的连接里执行脚本的普通程序:

private void ExecuteBatch(SqlConnection conn, string filename)

{

// Load the sql code to reset/build the database

//装载重置/构建数据训的sql代码

    System.IO.StreamReader r = System.IO.File.OpenText(filename);

    string sqlCmd = r.ReadToEnd();

    r.Close();

 

// Build & execute the command

//构建一个执行语句的command对象

    SqlCommand cmd = new SqlCommand(sqlCmd, conn);

    cmd.CommandType = CommandType.Text;

    cmd.ExecuteNonQuery();

}

This is pretty basic SQL interaction. This code is executed for the scripts resetdb.sql, createalbum.sql and createpics.sql. These script file contains all the necessary SQL to drop and then CREATE TABLE and ALTER TABLE statements to setup the database schema and stored procedures. Any error that occurs here will be thrown from the application and handled by the generic popup handler.

这是一个相当基本的SQL语句交互,这段代码是为了实现重置数据库,创建相册和创建相片的目的而执行的,这些脚本文件包含所有删除然后建表,改表的语句以及安装数据库架构和存贮过程的所有脚本,任何在这儿发生的错误都将会抛出异常并且被通用异常捕获器处理。

Populating the database

组装数据库

The database is built by compiling the scanned information from the file system into a DataSet object which is then committed to the database. First we setup the objects we will use for creating the albums table:

这个数据库会被构建通过编译一个扫描文件系统信息到一个数据集对象,然后把它提交到到数据库。首先我们要安装我们为了创建albums表而使用到的对象。

// The dataset

DataSet ds = new DataSet();

 

// The command object calls the stored proc which either does the insert or

// returns the existing row id. In either case

// the output parameter id is then

// used to update our existing DataTable object with the actual id.

insertAlbumCmd = new SqlCommand("CreateAlbum", conn);

insertAlbumCmd.CommandType = CommandType.StoredProcedure;

insertAlbumCmd.Parameters.Add("@rootpath", 

          SqlDbType.VarChar, 1024, "rootpath");

insertAlbumCmd.Parameters.Add("@description", 

          SqlDbType.VarChar, 256, "description");

insertAlbumCmd.Parameters.Add("@id", SqlDbType.Int, 0, "id");

insertAlbumCmd.Parameters["@id"].Direction = ParameterDirection.Output;

insertAlbumCmd.UpdatedRowSource = UpdateRowSource.OutputParameters;

We build the command which will invoke the stored procedure listed above. We tell ADO.NET that, after the insert, it should take the output parameter from the stored procedure and use this value (the identity value) to update the id column of the disconnected DataTable.

我们执行command对象,它将会调用上面列出的存贮过程,我们告诉ADO.NET那些后,在插入新记录后,它将会会从存贮程序中取提输出参数,然后使用这个值(唯一值)来更新断开连接的datatable里面的ID列值

// The adapter only needs to perform an insert. (Select is for FillSchema)

albumsAdapter = new SqlDataAdapter("SELECT * FROM albums", conn);

albumsAdapter.InsertCommand = insertAlbumCmd;

albumsAdapter.FillSchema(ds, SchemaType.Mapped, "albums");

DataTable albums = ds.Tables["albums"];

Here we attach the command to a SqlDataAdapter object and then pull the schema from the database into our table.

这儿我们把命令对象赋给数据适配器,然后会它会从我数据库里把数据架构拉到我们的表里面。

// Need to seed negative values to prevent dups during the insert when SQL

// generated values conflict with ADO.NET generated values

DataColumn dc = albums.Columns["id"];

dc.AutoIncrementSeed = -1;

dc.AutoIncrementStep = -1;

This part is important because it avoids any duplicate keys being generated during the batch update. If the SQL server returns an identity value which already exists in the DataTable, an exception would be thrown. Using negative identity values prevents this from ever happening.

这部分是很重要的,因为它避免了任何重复的键值在执行批量更新的时候,如果sqlserver数据库返回一个已经存在数据表的的唯一值,那么就会抛出一个异常,使用拒绝一样的值可以阻止这些的发生。

Finally we can get about doing the actual work:

最后我们能得到关于做真正工作的

string[] dirs = System.IO.Directory.GetDirectories(rootPath);

 

foreach (string dir in dirs)

{

    // Insert or update the album in the database

    string dirname = System.IO.Path.GetFileName(dir);

 

    // New row will populate the primary key for us

    DataRow dr = albums.NewRow();

    dr["rootpath"] = dir;

    dr["description"] = dirname;

    albums.Rows.Add(dr);

}

 

// Commit the albums to the database

albumsAdapter.Update(ds, "albums");

The Update will insert all pending rows into the data store.

Update操作会把所有未提交的行插入到数据库里央。

Populating the pictures table follows the same logic, so I won't repeat it here. For each *.jpg file found, a row is added to the DataTable and then the SqlDataUdapter Update method is invoked in order to perform the necessary inserts. The main difference are the columns required; for each image found, this method is called to collect the necessary data into the DataRow:

导入相片表执行的是同样的操作,因此我就不在这儿再重复它啦,对于每一个被找到的.jpg文件,一个新行就会被加进数据表里面,然后使用数据适配器的更新方法被调用为了执行必须的插入动作。主要的不同就是列要求不周,对于每一个找到的图片,这个方法都会被调用来搜集必需的数据到数据行里面。

protected void GetImageInfo(string imgpath, DataRow dr)

{

    // Get data about this pic and populate 

    // the data row for the insert

    System.IO.FileStream fs = File.Open(imgpath, 

         FileMode.Open, FileAccess.Read, FileShare.Read);

    Bitmap img = new Bitmap(fs);

    dr["filename"] = System.IO.Path.GetFileName(imgpath);

    dr["imgsize"] = (int)fs.Length;

    dr["height"] = img.Height;

    dr["width"] = img.Width;

    dr["imgdate"] = File.GetCreationTime(imgpath);

    dr["numviews"] = 0;

    img.Dispose();

    fs.Close();

}

Unfortunately this causes a major performance hit because each image must be loaded into memory in order to determine its dimensions. This is a one-time up-front operation, so this is an acceptable tradeoff.

不幸的是这也性能上的问题,因为为了决定图片的维数,于是每张图片都必须被装入内存,这是一个一次操作,因些这也是一个可以接收的折折衷方法。

The front-end

前端

The front-end is the actual ASP.NET application which pulls our data out from the database and formats it nicely for the user.

前端是真正的asp.net的程序,它从数据库中取出数据然后以友好的格式显现给用户。

The list of albums

相册列表:

The first thing the user sees is a list of albums, along with a little folder icon and a bit of information about the album itself (the number of pictures it contains). This is implemented with a DataList control. The control is defined here:

用户首先看到的是一个相册的列表,包括一些文件夹图标和一些关于相册的简短说明信息(它所包含的相片数量),这是用DataList控件实现的,它的定义如下:

<asp:DataList id="dl" runat="server" 
          RepeatDirection="Horizontal" RepeatColumns="3">
  <ItemTemplate>
  <table><tr><td><img src="folder.png"></td>
    <td><asp:HyperLink Runat="server" ID="hlItem" 
      NavigateUrl='<%# "viewalbum.aspx?id=" + 
             DataBinder.Eval(Container.DataItem, "id")%>' 
      Text='<%#DataBinder.Eval(Container.DataItem, 
             "description")%>'>
    </asp:HyperLink><br>
    <asp:Label Runat="server" ID="lbItem" 
               Text='<%# DataBinder.Eval(Container.DataItem, 
                             "piccount") + " pictures" %>'>
    </asp:Label></td>
  </tr></table>
  </ItemTemplate>
</asp:DataList>

The important thing to note here is that each item is comprised of a folder bitmap, a HyperLink control, and a Label control. The Hyperlink has its text bound to the description of the album, and its URL bound to the viewalbum.aspx page. It passes the album ID to the viewalbum.aspx in the URL.

在这里比较重要的就是每一个项都是由文件夹位图,超连接控件,Label控件组成。Hyperlink绑定它的TEXT属性到相册的描述上,它的URL地址绑定到页面viewalbum.aspx上,并且通过URL传相册的ID到页面viewalbum.aspx上。

The code behind for this file is all of two lines:

这个文件后端的代码只有如下的两行:

    // Load the albums table and bind to the datalist

    dl.DataSource = npdata.GetAlbums();

    dl.DataBind();

The GetAlbums method is defined in a class named npdata. The npdata class contains static methods which encapsulate the data access adapters and commands to interface with the SQL database. The GetAlbums method does a basic select and fill and returns the DataSet. You may notice that the Label control references the piccount column, which does not exist in our schema. The piccount column is a calculated value which you can see in the query we use to bind to the list:

方法GetAlbums被定义在类npdata里面,npdata的类里面封装了一些数据访问适配器和命令的对象,通过它们与数据库进行交互,GetAlbums方法只做基本的选择数据,填充数据,然后返回数据集,你可能已经注意到Label控件引用的并不存在我们的数据库架构里的列piccountpiccount列是一个在查询里能看到的用来组绑定到LIST的计算值

SqlDataAdpater adap = new SqlDataAdapter("SELECT *," +

                "(SELECT COUNT(*) FROM pics WHERE pics.albumid=albums.id) 

                AS piccount " +

                "FROM albums", conn);

So the piccount column is calculated by doing a sub-query on the pics table to determine how many pictures have a parent in the given album.

因些,在被给的相册里,列piccount通过在表pics里面执行子查询就获得每个根有多少相片。

Viewing an album

浏览相册

When the user clicks on an album, the NavigateUrl property from the HyperLink control directs the browser to viewalbum.aspx and the album ID is passed along in the URL. This page generates a thumbnail for each image along with a basic description, and allows the user to click the image or edit the image properties. We once again utilize the DataList control for this functionality and it operates much the same way. The one point of note in this DataList is the actual URL for the thumbnail image:

当用户单击相册的时候,超连接控件的地址属性就会指向浏览器到viewalbum.aspx页面,并且相册的ID也随着地址传过去,这个页面对每个相片都会生成细的基本描述,也允许用户单击图片或者编辑图片属性。我们再一次利用datalist控件来实现这个功能,它的操作绝大部分是相同的,要注意的一点是在datalist 里面小图片是实际的URL地址

<img border="0" src='<%# "genimage.ashx?thumbnail=y&id=" 
               + DataBinder.Eval(Container.DataItem, "id") %>'>

We can't link directly to the .jpg file because the server is not directly sharing the images folder. So we link to a page called genimage.ashx which implements a sort of proxy that accepts the picture ID and streams the actual image data back to the client. It also accepts a thumbnail parameter, which indicates that the image should be sized down to 150x150. Note the .ashx extension; these are special files containing directives that you easily implement your own IHttpHandler-derived class. These classes give you a low-level interface to send data back to the client without all the overhead of creating and managing the lifecycle of a Page object. Our .ashx file contains only one line:

我们不能直接连接到.jpg文件,因为服务器并不直接共享t图片文件夹,因些我们链接的页面称为genimage.ashx,它是一种接受返回客户端正的图像ID和图片数据的文件流的代理。它允许接收一个thumbnail参数,它指出图像应当是150*150的大小格式下载。要注意文件扩展名是ashx,这是一种特殊的包含能够让你容易的实现你自己接口的文件,这些类给你提供一个比较底层的实现发送数据到客户端的接口,而不用创建和管理所有上面的页面对象,我们的ashx文件只包含一行代码。

<%@ WebHandler Language="C#" Class="netpix.ImageGenerator" %>

This directs the client request for handling by the ImageGenerator class which is discussed in the next section.

它就是指出客户端的请求是通过下一节要讨论的ImageGenerator类来处理。

Generating the pictures

生成像片

The ImageGenerator class implements the IHttpHandler interface, which is a very simple and low-level interface for send raw streams of data back to the client. We are just dumping the bytes of image, so it's perfect for our needs. Since this class is key to the operation of the application, we will examine all of the code for this class:

imageGenerator实现了IHttpHandler接口,这是一个非常简单和底层的接口,用来发送原始的数据流到客户端,我们正要传的是图片的字节流,因为它刚好满足我们的需求,因些这个类是整个应用程序操作的关键,我们将会检查这个类的所有代码。

public class ImageGenerator : IHttpHandler 

{ 

    public bool IsReusable 

        { get { return true; } } 

 

    public void ProcessRequest(HttpContext Context) 

    { 

        // Get the image filename and album root path from the database

        int numviews;

        int picid = Convert.ToInt32(Context.Request["id"]);

        string imgpath = npdata.GetPathToPicture(picid, out numviews);

 

        // Writing an image to output stream

        Context.Response.ContentType = "image/jpg";

Here we retrieve the picture ID from the URL and invoke the GetPathToPicture method which wraps a SQL join statement that returns the full local path to the image, as well as the number of times the image has been viewed on the client. Then we set the content type to jpg because we are impersonating a jpg file.

这儿,我们重新从URL中得到 像片的ID,然后调用封装了SQK连接语句的GetPathToPicture方法来返回图像完整的本地路径,和已经在客户端被浏览的次数。然后我们设置内容类型为 jpg,因为我们正在格式化一个jpg文件。

    // 'thumbnail' means we are requesting a thumbnail

    if (Context.Request["thumbnail"] != null)

    {

        // Need to load the image, resize it, and stream to the client.

        // Calculate the scale so as not to stretch or distort the image.

        Bitmap bmp = new Bitmap(imgpath);

        float scale = 150.0f / System.Math.Max(bmp.Height, bmp.Width);

        System.Drawing.Image thumb = bmp.GetThumbnailImage(

            (int)(bmp.Width * scale), (int)(bmp.Height * scale), 

            null, System.IntPtr.Zero);

        thumb.Save(Context.Response.OutputStream, 

            System.Drawing.Imaging.ImageFormat.Jpeg);

        bmp.Dispose();

        thumb.Dispose();

    }

In the case where the Request URL contains the thumbnail parameter, we first load the image file from disk and call GetThumbnailImage to scale it down. We scale it down by a constant factor to maintain the aspect ratio so as not to distort the image. We then save the resized image directly to the Response object's output stream. This puts a pretty heavy stress on the server CPU when a large number of thumbnails are requested (I will discuss this in the 'Future Items' section).

在请求URL里包含了thumbnuil参数,我们首先从磁盘装载图片文件,然后调用GetThumbnailImage来缩放文件,我们是通过一个常数因子来维护它的外 观比率,从从而不至于使用图像失真,然后我们保存这个引人缩小了的图像到Response对象的输出流里,当大量的thumbnails被客户端请求的时候,就会给服务器的CUP增加一些压力。(我将会在Future Items’ Section里面讨论这一点。

    else

    {

        // Stream directly from the file

        System.IO.FileStream fs = File.Open(imgpath, 

            FileMode.Open, FileAccess.Read, FileShare.Read);

 

        // Copy it out in chunks

        const int byteLength = 16384;

        byte[] bytes = new byte[byteLength];

        while( fs.Read(bytes, 0, byteLength ) != 0 )

        {

            Context.Response.BinaryWrite(bytes); 

        }

        fs.Close();

 

        // Now increment the view counter in the database

        npdata.SetNumViews(picid, numviews+1);

    }

}

In this case, we are interested in streaming the image directly from the file contents. The current implementation reads the file contents in chunks and sends them to the response's output stream. We also need to increment the picture's view count in the database because the full-size image has been requested. The SetNumViews method just issues an SQL UPDATE statement to the pics table to set the numviews column for the given picture.

在这部分里,我们感兴趣的是直接从文件目录里面流化图像,当前实现的就是从文件块里读然后发送它们到response的输出流里。同时我们也要增加数据库里面图像被浏览的的次数,因为完整的图片已经被客户请求啦,SetNumViews方法执行了一个SQLUpdate语句对表pics,设置列numviews为被访问的图片。

Viewing and editing a Picture

浏览和编辑图像

From the album view, the user can either view an image or edit the image information. There really isn't much of interest happening in viewpic.aspx or editpic.aspx. The user can supply title and description information in the editor, which will then be used by the viewer. The viewer will show the title for a picture if available, otherwise it will default to the filename. This is accomplished in the SQL statement:

从这个相册的浏览,用户既能够浏览图像也能够编辑图像信息.用户能够提供主题和描述信息在编辑器里,然后就会用用在浏览器里,浏览器也会显示主题对一张可用的图片。否则的话,它将缺省使用文件名,这些会在SQL语句里面被完成。

    // The command to select a specific picture data

    getpicinfo = new SqlCommand("SELECT ISNULL(title, filename) 

                       AS returntitle, " +

                       "ISNULL([description],'') AS returndesc "+

                       "FROM pics WHERE pics.id=@picid", conn);

    getpicinfo.Parameters.Add("@picid", SqlDbType.Int);

Our schema design dictates that we will use the DB Null value to indicate no custom title is present. When this happens, we use the image filename as the text for the picture's caption.

引至,http://www.codeproject.com/aspnet/NetPix.asp

原文及代码 

原创粉丝点击