1.什么是ContentProvider

 数据库在Android其中是私有的,当然这些数据包含文件数据和数据库数据以及一些其它类型的数据。

 不能将数据库设为WORLD_READABLE,每一个数据库都仅仅能创建它的包訪问,

 这意味着仅仅有由创建数据库的进程可訪问它。假设须要在进程间传递数据,

 则能够使用AIDL/Binder或创建一个ContentProvider,可是不能跨越进程/包边界直接来使用数据库。

 一个Content Provider类实现了一组标准的方法接口,从而可以让其它的应用保存或读取此Content Provider的各种数据类型。

 也就是说,一个程序能够通过实现一个Content Provider的抽象接口将自己的数据暴露出去。

 外界根本看不到,也不用看到这个应用暴露的数据在应用其中是怎样存储的,或者是用数据库存储还是用文件存储,还是通过网上获得,这些一切都不重要,

 重要的是外界能够通过这一套标准及统一的接口和程序里的数据打交道,能够读取程序的数据,也能够删除程序的数据,

 当然,中间也会涉及一些权限的问题。下边列举一些较常见的接口,这些接口例如以下所看到的。

·  query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder):通过Uri进行查询,返回一个Cursor。

·  insert(Uri url, ContentValues values):将一组数据插入到Uri 指定的地方。

·  update(Uri uri, ContentValues values, String where, String[] selectionArgs):更新Uri指定位置的数据。

·  delete(Uri url, String where, String[] selectionArgs):删除指定Uri而且符合一定条件的数据。

2.什么是ContentResolver

外界的程序通过ContentResolver接口能够訪问ContentProvider提供的数据,在Activity其中通过getContentResolver()能够得到当前应用的 ContentResolver实例。

 ContentResolver提供的接口和ContentProvider中须要实现的接口相应,主要有下面几个。

·  query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder):通过Uri进行查询,返回一个Cursor。

·  insert(Uri url, ContentValues values):将一组数据插入到Uri 指定的地方。

·  update(Uri uri, ContentValues values, String where, String[] selectionArgs):更新Uri指定位置的数据。

·  delete(Uri url, String where, String[] selectionArgs):删除指定Uri而且符合一定条件的数据。

3.ContentProvider和ContentResolver中用到的Uri

在ContentProvider和 ContentResolver其中用到了Uri的形式通常有两种,一种是指定所有数据,还有一种是指定某个ID的数据。

我们看以下的样例。

·  content://contacts/people/  这个Uri指定的就是所有的联系人数据。

·  content://contacts/people/1 这个Uri指定的是ID为1的联系人的数据。 

在上边两个类中用到的Uri一般由3部分组成。

·  第一部分是方案:"content://" 这部分永远不变

·  第二部分是授权:"contacts"

·  第二部分是路径:"people/","people/1"(假设没有指定ID,那么表示返回所有)。

因为URI通常比較长,并且有时候easy出错,且难以理解。所以,在Android其中定义了一些辅助类,并且定义了一些常量来取代这些长字符串的使用,比例如以下边的代码:

·  Contacts.People.CONTENT_URI (联系人的URI)。

在我们的实例MyProvider中是例如以下定义的:

public static final String AUTHORITY="com.teleca.PeopleProvider";

public static final String PATH_SINGLE="people/#";

public static final String PATH_MULTIPLE="people";

public static final Uri content_URI=Uri.parse("content://"+AUTHORITY+"/"+PATH_MULTIPLE);

实例1:

文件MyProvider.java

package com.teleca.provider;

import java.util.HashMap;

import android.content.ContentProvider;

import android.content.ContentUris;

import android.content.ContentValues;

import android.content.Context;

import android.content.UriMatcher;

import android.database.Cursor;

import android.database.SQLException;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

import android.database.sqlite.SQLiteQueryBuilder;

import android.net.Uri;

import android.text.TextUtils;

import android.util.Log;

public class MyProvider extends ContentProvider {

 public static final String MIME_DIR_PREFIX="vnd.android.cursor.dir";

 public static final String MIME_ITEM_PREFIX="vnd.android.cursor.item";

 public static final String MIME_ITEM="vnd.msi.people";

 public static final String MIME_TYPE_SINGLE=MIME_ITEM_PREFIX+"/"+MIME_ITEM;

 public static final String MIME_TYPE_MULTIPLE=MIME_DIR_PREFIX+"/"+MIME_ITEM;

 public static final String AUTHORITY="com.teleca.PeopleProvider";

 public static final String PATH_SINGLE="people/#";

 public static final String PATH_MULTIPLE="people";

 public static final Uri content_URI=Uri.parse("content://"+AUTHORITY+"/"+PATH_MULTIPLE);

 public static final String DEFAULT_SORT_ORDER="name DESC";

 public static final String _ID="_id";

 public static final String NAME="name";

 public static final String PHONE="phone";

 public static final String AGE="age";

 public static final int PEOPLE=1;

 public static final int PEOPLES=2;

 private static UriMatcher URI_MATCHER;

 private static HashMap<String,String> PROJECTION_MAP;

 public static String DB_NAME="peopledb";

 public static String DB_TABLE_NAME="people";

 SQLiteDatabase db;

 DBOpenHelper dbOpenHelper;

 static 

 {

  URI_MATCHER=new UriMatcher(UriMatcher.NO_MATCH);

  URI_MATCHER.addURI(AUTHORITY, PATH_MULTIPLE, PEOPLES);

  URI_MATCHER.addURI(AUTHORITY, PATH_SINGLE, PEOPLE);

  PROJECTION_MAP=new HashMap<String,String>();

  PROJECTION_MAP.put(_ID, "_id");

  PROJECTION_MAP.put(NAME, "name");

  PROJECTION_MAP.put(PHONE, "phone");

  PROJECTION_MAP.put(AGE, "age");

 }

 @Override

 public int delete(Uri uri, String selection, String[] selectionArgs) {

  // TODO Auto-generated method stub

  int count=0;

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   count=db.delete(DB_TABLE_NAME,  selection, selectionArgs);

   break;

  case PEOPLE:

   String segment =uri.getPathSegments().get(1);

   String where="";

   if(!TextUtils.isEmpty(selection))

   {

    where=" AND ("+selection+")";

   }

   count=db.delete(DB_TABLE_NAME, "_id="+segment+where, selectionArgs);

   break;

  default:

   throw new IllegalArgumentException("Unkonw URI"+uri);

  }

  getContext().getContentResolver().notifyChange(uri, null);//@2

  return count;

 }

 @Override

 public String getType(Uri uri) {

  // TODO Auto-generated method stub

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   return MIME_TYPE_MULTIPLE;

  case PEOPLE:

   return MIME_TYPE_SINGLE;

  default:

   throw new IllegalArgumentException("Unkown URI "+uri);

  }

 }

 @Override

 public Uri insert(Uri uri, ContentValues values) {

  // TODO Auto-generated method stub

  long rowId=0L;

  if(URI_MATCHER.match(uri)!=PEOPLES)

  {

   throw new IllegalArgumentException("Unkown URI"+uri);

  }

  rowId=db.insert(DB_TABLE_NAME, null, values);

  if(rowId>0)

  {

   Uri result=ContentUris.withAppendedId(content_URI, rowId);

   getContext().getContentResolver().notifyChange(result, null);//@2

   return result;

  }

  else

   throw new SQLException("Failed to insert row into "+uri);

 }

 @Override

 public boolean onCreate() {

  // TODO Auto-generated method stub

  dbOpenHelper=new DBOpenHelper(this.getContext(),DB_NAME,1);

  db=dbOpenHelper.getWritableDatabase();

  return false;

 }

 @Override

 public Cursor query(Uri uri, String[] projection, String selection,

   String[] selectionArgs, String sortOrder) {

  // TODO Auto-generated method stub

  SQLiteQueryBuilder queryBuilder=new SQLiteQueryBuilder();

  queryBuilder.setTables(DBInfo.DB_TABLE_NAME);

  queryBuilder.setProjectionMap(PROJECTION_MAP);

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   break;

  case  PEOPLE:

   queryBuilder.appendWhere("_id="+uri.getPathSegments().get(1));

   break;

  default:

   throw new IllegalArgumentException("Unkonw URI"+uri);

  }

  String orderBy=null;

  if(TextUtils.isEmpty(sortOrder))

  {

   orderBy=DEFAULT_SORT_ORDER;

  }

  else

   orderBy=sortOrder;

  Cursor c=queryBuilder.query(db, projection, selection, selectionArgs, null, null, orderBy);

  c.setNotificationUri(getContext().getContentResolver(), uri);//@1

  return c;

 }

 @Override

 public int update(Uri uri, ContentValues values, String selection,

   String[] selectionArgs) {

  // TODO Auto-generated method stub

  int count=0;

  switch(URI_MATCHER.match(uri))

  {

  case PEOPLES:

   count=db.update(DB_TABLE_NAME, values, selection, selectionArgs);

   break;

  case PEOPLE:

   String segment =uri.getPathSegments().get(1);

   String where="";

   if(!TextUtils.isEmpty(selection))

   {

    where=" AND ("+selection+")";

   }

   count=db.update(DB_TABLE_NAME, values, "_id="+segment+where, selectionArgs);

   break;

  default:

   throw new IllegalArgumentException("Unkonw URI"+uri);

  }

  getContext().getContentResolver().notifyChange(uri, null);//@2

  return count;

 }

}

class DBOpenHelper extends SQLiteOpenHelper

{

 private static final String DB_CREATE="CREATE TABLE "

  +DBInfo.DB_TABLE_NAME

  +" (_id INTEGER PRIMARY KEY,name TEXT UNIQUE NOT NULL,"

  +"phone TEXT,age INTEGER);";

 final static String tag="hubin";

 public DBOpenHelper(Context context,String dbName,int version)

 {

  super(context,dbName,null,version);

 }

 public void onCreate(SQLiteDatabase db)

 {

  try{

   db.execSQL(DB_CREATE);

  }

  catch(SQLException e )

  {

   Log.e(tag,"",e);

  }

 }

 public void onOpen(SQLiteDatabase db)

 {

  super.onOpen(db);

 }

 public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion)

 {

  db.execSQL("DROP TABLE IF EXISTS "+DBInfo.DB_TABLE_NAME);

  this.onCreate(db);

 }

}

class DBInfo

{

 public static String DB_NAME="peopledb";

 public static String DB_TABLE_NAME="people"; 

}

注意1:c.setNotificationUri(getContext().getContentResolver(), uri);

这里是把Cursor C加入到ContentResolver的监督对象组中去。

一旦有与uri相关的变化,ContentResolver就回通知Cursor C.

可能Cursor有个私有的内部类ContentObserver的实现。ContentResolver是通过该类来通知Cursor的。

public abstract void  setNotificationUri  (ContentResolver  cr, Uri  uri)

Register to watch a content URI for changes. This can be the URI of a specific data row (for example, "content://my_provider_type/23"), 

or a a generic URI for a content type.

Parameters

cr  The content resolver from the caller's context. The listener attached to this resolver will be notified.

uri  The content URI to watch. 

注意2: getContext().getContentResolver().notifyChange(uri, null)

通知数据发生了变化。

  public void  notifyChange  (Uri  uri, ContentObserver  observer)

Notify registered observers that a row was updated. To register, call registerContentObserver(). By default, CursorAdapter objects will get this notification.

Parameters

observer  The observer that originated the change, may be null 

这里为null的意思可能就是调用在ContentResolver中注冊的ContentObserver,反之则是调用參数指定的

文件People.java

package com.teleca.provider;

public class People {

 public long id;

 public String name;

 public String phone;

 public int age;

}

文件

Hello.java

package com.teleca.provider;

import java.util.ArrayList;

import java.util.List;

import android.app.Activity;

import android.content.ContentUris;

import android.content.ContentValues;

import android.database.Cursor;

import android.database.SQLException;

import android.net.Uri;

import android.os.Bundle;

import android.os.Handler;

import android.util.Log;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.Button;

public class Hello extends Activity {

    /** Called when the activity is first created. */

 final static String tag="hubin";

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

  Button button = (Button) findViewById(R.id.Button01);

  OnClickListener listener = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_ADD;

    doAction();

   }

  };

  button.setOnClickListener(listener);

  Button button2 = (Button) findViewById(R.id.Button02);

  OnClickListener listener2 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_UPDATE;

    doAction();

   }

  };

  button2.setOnClickListener(listener2);

  Button button3 = (Button) findViewById(R.id.Button03);

  OnClickListener listener3 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_QUERY;

    doAction();

   }

  };

  button3.setOnClickListener(listener3);

  Button button4 = (Button) findViewById(R.id.Button04);

  OnClickListener listener4 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_QUERY_ALL;

    doAction();

   }

  };

  button4.setOnClickListener(listener4);

  Button button5 = (Button) findViewById(R.id.Button05);

  OnClickListener listener5 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_DELETE;

    doAction();

   }

  };

  button5.setOnClickListener(listener5);

  Button button6 = (Button) findViewById(R.id.Button06);

  OnClickListener listener6 = new OnClickListener() {

   @Override

   public void onClick(View v) {

    cmd = CMD_DELETE_ALL;

    doAction();

   }

  };

  button6.setOnClickListener(listener6);

  mHandler = new Handler();

    }

 int cnt = 0;

 private Handler mHandler;

 int cmd = 0;

 final int CMD_ADD = 1;

 final int CMD_UPDATE = 2;

 final int  CMD_QUERY= 3;

 final int CMD_QUERY_ALL = 4;

 final int CMD_DELETE = 5;

 final int CMD_DELETE_ALL = 6;

 People people=new People();

 final static String projection[]=new String[]

                                       {"_id","name","phone","age"};

 class DatabaseThread implements Runnable {

  public void run() {

   if (cmd == CMD_ADD) {

    people.name="robin"+System.currentTimeMillis()%100;

    people.phone=""+System.currentTimeMillis();

    people.age=1;

    ContentValues values=new ContentValues();

    values.put("name", people.name);

    values.put("phone", people.phone);

    values.put("age", people.age);

    Uri uri=getContentResolver().insert(MyProvider.content_URI, values);

    people.id=ContentUris.parseId(uri);

    Log.i("hubin",uri.toString());

   } else if (cmd == CMD_UPDATE) {

    ContentValues values=new ContentValues();

    people.phone=""+System.currentTimeMillis();

    values.put("phone", people.phone);

    Uri uri=ContentUris.withAppendedId(MyProvider.content_URI, people.id);

    getContentResolver().update(uri,values,null,null);

   } else if (cmd == CMD_QUERY) {

    Uri uri=ContentUris.withAppendedId(MyProvider.content_URI, people.id);

    Cursor c=getContentResolver().query(uri, projection, null, null, null);

    People p=get(c);

    printPeople(p);

   } else if (cmd == CMD_QUERY_ALL) {

    Uri uri=MyProvider.content_URI;

    Cursor c=getContentResolver().query(uri, projection, null, null, null);

    List<People> list=getAll(c);

    int total=list.size();

    for(int i=0;i<total;i++)

    {

     printPeople(list.get(i));

    }

   }

   else if (cmd==CMD_DELETE)

   {

    Uri uri=ContentUris.withAppendedId(MyProvider.content_URI, people.id);

    getContentResolver().delete(uri, null, null);

   }

   else if (cmd==CMD_DELETE_ALL)

   {

    Uri uri=MyProvider.content_URI;

    getContentResolver().delete(uri, null, null);

   }

   cnt++;

  }

 }

 void printPeople(People p)

 {

 Log.i(tag, "id:"+p.id);

 Log.i(tag, "name:"+p.name);

 Log.i(tag,"phone:"+p.phone);

 Log.i(tag,"age:"+p.age);

 }

 DatabaseThread dataDealer=new DatabaseThread();

 void doAction() {

  mHandler.post(dataDealer);

 }

 public People get(Cursor c)

 {

  People people=new People();

  try{

   Log.i(tag,"count:"+c.getCount());

   if(c.getCount()>0)

   {

    c.moveToFirst();

    people=new People();

    people.id=c.getLong(0);

    people.name=c.getString(1);

    people.phone=c.getString(2);

    people.age=c.getInt(3);

   }

  }catch(SQLException e)

  {

   Log.i(tag,"",e);

  }

  finally

  {

   if(c!=null&&!c.isClosed())

   {

    c.close();

   }

  }

  return people;

 }

 public List<People> getAll(Cursor c)

 {

  ArrayList<People> ret=new ArrayList<People>();

  try

  {

   int count=c.getCount();

   c.moveToFirst();

   People people;

   for(int i=0;i<count;i++)

   {

    people=new People();

    people.id=c.getLong(0);

    people.name=c.getString(1);

    people.phone=c.getString(2);

    people.age=c.getInt(3);

    ret.add(people);

    c.moveToNext();

   }

   

  }catch(SQLException e)

  {

   Log.i(tag,"",e);

  }

  finally

  {

   if(c!=null&&!c.isClosed())

   {

    c.close();

   }

  }

  return ret;

 }

}

注意:Cursor c不用时要关掉。

文件AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

      package="com.teleca.provider"

      android:versionCode="1"

      android:versionName="1.0">

    <application android:icon="@drawable/icon" android:label="@string/app_name">

        <activity android:name=".Hello"

                  android:label="@string/app_name">

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    <provider android:syncable="true" android:name="MyProvider" android:authorities="com.teleca.PeopleProvider"></provider>

</application>

    <uses-sdk android:minSdkVersion="7" />

</manifest>

list_row.xml文件 
<?xml version="1.0" encoding="utf-8"?> 
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" android:id="@+id/list_row"
/>
main.xml文件
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > 
<ListView android:id="@id/android:list" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#00FF00" android:layout_weight="1" android:drawSelectorOnTop="false"/> 
<Button android:text="@+string/Add" android:id="@+id/Button01" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
<Button android:text="@+string/DeleteAll" android:id="@+id/Button02" android:layout_width="wrap_content"></Button>
</LinearLayout>
须要的权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
 <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>

真的须要这些权限?为什么须要呢?或许是我曾经写错了吧

ContentProvider简单介绍的更多相关文章

  1. Intent的作用和表现形式简单介绍

    Intent的作用和表现形式简单介绍 1.描写叙述:Intent负责相应用中一次操作的动作,动作涉及的数据,附加数据进行描写叙述.系统或者应用依据此Intent的描写叙述,负责找到相应的组件,将Int ...

  2. ANDROID培训准备资料之四大组件的简单介绍

    Android四大组件是一个android app 最基本的组成部分,这篇博客主要给大家简单的介绍一下四种组件 (1)Activities (2)Services (3)BroadcastReceiv ...

  3. [原创]关于mybatis中一级缓存和二级缓存的简单介绍

    关于mybatis中一级缓存和二级缓存的简单介绍 mybatis的一级缓存: MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候 ...

  4. 利用Python进行数据分析(7) pandas基础: Series和DataFrame的简单介绍

    一.pandas 是什么 pandas 是基于 NumPy 的一个 Python 数据分析包,主要目的是为了数据分析.它提供了大量高级的数据结构和对数据处理的方法. pandas 有两个主要的数据结构 ...

  5. 利用Python进行数据分析(4) NumPy基础: ndarray简单介绍

    一.NumPy 是什么 NumPy 是 Python 科学计算的基础包,它专为进行严格的数字处理而产生.在之前的随笔里已有更加详细的介绍,这里不再赘述. 利用 Python 进行数据分析(一)简单介绍 ...

  6. yii2的权限管理系统RBAC简单介绍

    这里有几个概念 权限: 指用户是否可以执行哪些操作,如:编辑.发布.查看回帖 角色 比如:VIP用户组, 高级会员组,中级会员组,初级会员组 VIP用户组:发帖.回帖.删帖.浏览权限 高级会员组:发帖 ...

  7. angular1.x的简单介绍(二)

    首先还是要强调一下DI,DI(Denpendency Injection)伸手获得,主要解决模块间的耦合关系.那么模块是又什么组成的呢?在我看来,模块的最小单位是类,多个类的组合就是模块.关于在根模块 ...

  8. Linux的简单介绍和常用命令的介绍

    Linux的简单介绍和常用命令的介绍 本说明以Ubuntu系统为例 Ubuntu系统的安装自行百度,或者参考http://www.cnblogs.com/CoderJYF/p/6091068.html ...

  9. iOS-iOS开发简单介绍

    概览 终于到了真正接触IOS应用程序的时刻了,之前我们花了很多时间去讨论C语言.ObjC等知识,对于很多朋友而言开发IOS第一天就想直接看到成果,看到可以运行的IOS程序.但是这里我想强调一下,前面的 ...

随机推荐

  1. Android打开系统的Document文档图片选择

    打开Document UI 过滤图片 private void startAcitivty() { Intent intent = new Intent(); intent.setAction(&qu ...

  2. c - 向一个排序好的数组插入一个数,插入后数组依然是排序好的

    概述 因为这里只是演示插入1个数,这里我不用malloc再重新分配,而是将原先数组的腾出一个占位符. 完整代码如下: #include <stdio.h> #define LEN 6 // ...

  3. 启动php-fpm时报错

    [root@localhost ~]# /usr/local/php/sbin/php-fpm [06-Aug-2012 19:17:53] ALERT: [pool www] pm.min_spar ...

  4. UITableView-FDTemplateLayoutCell自动计算UITableView高度的使用

    基本应用如果你有self-satisfied cell,那么你应该做的是:#import "UITableView+FDTemplateLayoutCell.h"- (CGFloa ...

  5. HTTP 状态响应码 意思详解/大全

    HTTP 状态响应码 意思详解/大全 转:http://blog.csdn.net/helin916012530/article/details/29842595 HTTP状态码(HTTP Statu ...

  6. 高级I/O函数(3)-tee、fcntl函数

    tee函数使用 功能描述:tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作.它不消耗数据,因此源文件描述符仍然可以用于后续的操作. 函数原型: #include <fcntl.h> ...

  7. openMPI小集群安装

    经过一天的努力,终于完成了openMPI的多节点安装,即小集群安装.本文使用的是openmpi-1.6.5,下载地址见:http://www.open-mpi.org/software/ompi/v1 ...

  8. java web 学习(2)

    今天突然想到写的测试代码最好随时取出来,在不同的机器上不用老是拷来拷去,还真找着了免费的Svn, svn://www.svn999.com/luhouxiang.javastudy,暂时学习的工程代码 ...

  9. 学习用CMake来编写Qt程序

    最近开始学习CMake,因为项目需求需要用到Qt,自带的qmake会出现许多问题(比如文件修改之后有时候qmake不会侦测到不会重新编译,需要手动去编译等),于是开始尝试使用CMake来编写Qt程序, ...

  10. 关于highcharts(功能强大、开源、美观、图表丰富、兼容绝大多数浏览器的纯js图表库)

    官网http://www.hcharts.cn/ 引入下列文件 <script type="text/javascript" src="http://cdn.hch ...